athena-python-docx 0.1.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.
- athena_python_docx-0.1.0.dist-info/METADATA +76 -0
- athena_python_docx-0.1.0.dist-info/RECORD +18 -0
- athena_python_docx-0.1.0.dist-info/WHEEL +4 -0
- docx/__init__.py +16 -0
- docx/_batching.py +86 -0
- docx/api.py +41 -0
- docx/client.py +230 -0
- docx/document.py +335 -0
- docx/enum/__init__.py +1 -0
- docx/enum/table.py +15 -0
- docx/enum/text.py +29 -0
- docx/errors.py +30 -0
- docx/shared.py +81 -0
- docx/table.py +213 -0
- docx/text/__init__.py +8 -0
- docx/text/paragraph.py +141 -0
- docx/text/run.py +187 -0
- docx/typing.py +30 -0
docx/table.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Table, _Row, _Column, _Cell — python-docx parity.
|
|
2
|
+
|
|
3
|
+
Phase 1 surface:
|
|
4
|
+
Table.rows, Table.columns, Table.cell(row_idx, col_idx)
|
|
5
|
+
Table.add_row(), Table.add_column()
|
|
6
|
+
Table.style (get/set)
|
|
7
|
+
_Cell.text (get/set)
|
|
8
|
+
_Cell.merge(other_cell)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from docx._batching import run_sync
|
|
17
|
+
from docx.errors import ValidationError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from docx.client import Session
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _log_warn(msg: str) -> None:
|
|
24
|
+
print(f"[docx-sdk] WARN: {msg}", file=sys.stderr)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Table:
|
|
28
|
+
def __init__(self, *, session: "Session", node_id: str) -> None:
|
|
29
|
+
self._session: "Session" = session
|
|
30
|
+
self._node_id: str = node_id
|
|
31
|
+
|
|
32
|
+
def _fresh_node_id(self) -> str:
|
|
33
|
+
"""Re-find this table by its position. Superdoc rotates nodeIds
|
|
34
|
+
after style/option changes (see superdoc_write_utils.py:942-948).
|
|
35
|
+
"""
|
|
36
|
+
find_result: dict = run_sync(
|
|
37
|
+
self._session.doc.find({"type": "table"}),
|
|
38
|
+
)
|
|
39
|
+
items: list[dict] = find_result.get("items", [])
|
|
40
|
+
for item in items:
|
|
41
|
+
addr: dict = item.get("address", {}) if isinstance(item, dict) else {}
|
|
42
|
+
if addr.get("nodeId") == self._node_id:
|
|
43
|
+
return self._node_id
|
|
44
|
+
# Not found by exact id — trust the LAST table as ours, with a warning
|
|
45
|
+
if items:
|
|
46
|
+
last_addr: dict = (
|
|
47
|
+
items[-1].get("address", {}) if isinstance(items[-1], dict) else {}
|
|
48
|
+
)
|
|
49
|
+
new_id: str = str(last_addr.get("nodeId", ""))
|
|
50
|
+
if new_id:
|
|
51
|
+
_log_warn(
|
|
52
|
+
f"Table nodeId rotated: {self._node_id} -> {new_id}",
|
|
53
|
+
)
|
|
54
|
+
self._node_id = new_id
|
|
55
|
+
return self._node_id
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def rows(self) -> list["_Row"]:
|
|
59
|
+
nid: str = self._fresh_node_id()
|
|
60
|
+
info_obj: object = run_sync(
|
|
61
|
+
self._session.doc.tables.get({"nodeId": nid}),
|
|
62
|
+
)
|
|
63
|
+
info: dict = info_obj if isinstance(info_obj, dict) else {}
|
|
64
|
+
row_count: int = int(info.get("rows", 0))
|
|
65
|
+
return [_Row(table=self, index=i) for i in range(row_count)]
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def columns(self) -> list["_Column"]:
|
|
69
|
+
nid: str = self._fresh_node_id()
|
|
70
|
+
info_obj: object = run_sync(
|
|
71
|
+
self._session.doc.tables.get({"nodeId": nid}),
|
|
72
|
+
)
|
|
73
|
+
info: dict = info_obj if isinstance(info_obj, dict) else {}
|
|
74
|
+
col_count: int = int(info.get("cols", 0))
|
|
75
|
+
return [_Column(table=self, index=j) for j in range(col_count)]
|
|
76
|
+
|
|
77
|
+
def cell(self, row_idx: int, col_idx: int) -> "_Cell":
|
|
78
|
+
return _Cell(table=self, row=row_idx, col=col_idx)
|
|
79
|
+
|
|
80
|
+
def add_row(self) -> "_Row":
|
|
81
|
+
nid: str = self._fresh_node_id()
|
|
82
|
+
row_count: int = len(self.rows)
|
|
83
|
+
if row_count < 1:
|
|
84
|
+
raise ValidationError(
|
|
85
|
+
f"Cannot add row to empty table {nid}",
|
|
86
|
+
)
|
|
87
|
+
run_sync(
|
|
88
|
+
self._session.doc.tables.insert_row(
|
|
89
|
+
{
|
|
90
|
+
"tableNodeId": nid,
|
|
91
|
+
"rowIndex": row_count - 1,
|
|
92
|
+
"position": "below",
|
|
93
|
+
},
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
return _Row(table=self, index=row_count)
|
|
97
|
+
|
|
98
|
+
def add_column(self) -> "_Column":
|
|
99
|
+
nid: str = self._fresh_node_id()
|
|
100
|
+
col_count: int = len(self.columns)
|
|
101
|
+
if col_count < 1:
|
|
102
|
+
raise ValidationError(
|
|
103
|
+
f"Cannot add column to empty table {nid}",
|
|
104
|
+
)
|
|
105
|
+
run_sync(
|
|
106
|
+
self._session.doc.tables.insert_column(
|
|
107
|
+
{
|
|
108
|
+
"tableNodeId": nid,
|
|
109
|
+
"columnIndex": col_count - 1,
|
|
110
|
+
"position": "right",
|
|
111
|
+
},
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
return _Column(table=self, index=col_count)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def style(self) -> str | None:
|
|
118
|
+
nid: str = self._fresh_node_id()
|
|
119
|
+
info_obj: object = run_sync(
|
|
120
|
+
self._session.doc.tables.get({"nodeId": nid}),
|
|
121
|
+
)
|
|
122
|
+
info: dict = info_obj if isinstance(info_obj, dict) else {}
|
|
123
|
+
style_id: str = str(info.get("styleId", ""))
|
|
124
|
+
return style_id or None
|
|
125
|
+
|
|
126
|
+
@style.setter
|
|
127
|
+
def style(self, value: str | None) -> None:
|
|
128
|
+
nid: str = self._fresh_node_id()
|
|
129
|
+
run_sync(
|
|
130
|
+
self._session.doc.tables.set_style(
|
|
131
|
+
{"nodeId": nid, "styleId": value or "TableGrid"},
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class _Row:
|
|
137
|
+
def __init__(self, *, table: Table, index: int) -> None:
|
|
138
|
+
self._table: Table = table
|
|
139
|
+
self._index: int = index
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def cells(self) -> list["_Cell"]:
|
|
143
|
+
col_count: int = len(self._table.columns)
|
|
144
|
+
return [
|
|
145
|
+
_Cell(table=self._table, row=self._index, col=j)
|
|
146
|
+
for j in range(col_count)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class _Column:
|
|
151
|
+
def __init__(self, *, table: Table, index: int) -> None:
|
|
152
|
+
self._table: Table = table
|
|
153
|
+
self._index: int = index
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def cells(self) -> list["_Cell"]:
|
|
157
|
+
row_count: int = len(self._table.rows)
|
|
158
|
+
return [
|
|
159
|
+
_Cell(table=self._table, row=i, col=self._index)
|
|
160
|
+
for i in range(row_count)
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class _Cell:
|
|
165
|
+
def __init__(self, *, table: Table, row: int, col: int) -> None:
|
|
166
|
+
self._table: Table = table
|
|
167
|
+
self._row: int = row
|
|
168
|
+
self._col: int = col
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def text(self) -> str:
|
|
172
|
+
nid: str = self._table._fresh_node_id()
|
|
173
|
+
cell_obj: object = run_sync(
|
|
174
|
+
self._table._session.doc.tables.get_cell(
|
|
175
|
+
{"tableNodeId": nid, "row": self._row, "col": self._col},
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
if not isinstance(cell_obj, dict):
|
|
179
|
+
return ""
|
|
180
|
+
return str(cell_obj.get("text", ""))
|
|
181
|
+
|
|
182
|
+
@text.setter
|
|
183
|
+
def text(self, value: str) -> None:
|
|
184
|
+
nid: str = self._table._fresh_node_id()
|
|
185
|
+
run_sync(
|
|
186
|
+
self._table._session.doc.tables.update_cell_text(
|
|
187
|
+
{
|
|
188
|
+
"tableNodeId": nid,
|
|
189
|
+
"row": self._row,
|
|
190
|
+
"col": self._col,
|
|
191
|
+
"text": value,
|
|
192
|
+
},
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def merge(self, other: "_Cell") -> "_Cell":
|
|
197
|
+
if other._table is not self._table:
|
|
198
|
+
raise ValidationError(
|
|
199
|
+
"Cannot merge cells from different tables.",
|
|
200
|
+
)
|
|
201
|
+
nid: str = self._table._fresh_node_id()
|
|
202
|
+
run_sync(
|
|
203
|
+
self._table._session.doc.tables.merge_cells(
|
|
204
|
+
{
|
|
205
|
+
"tableNodeId": nid,
|
|
206
|
+
"startRow": self._row,
|
|
207
|
+
"startCol": self._col,
|
|
208
|
+
"endRow": other._row,
|
|
209
|
+
"endCol": other._col,
|
|
210
|
+
},
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
return self
|
docx/text/__init__.py
ADDED
docx/text/paragraph.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Paragraph — mirrors python-docx's Paragraph class.
|
|
2
|
+
|
|
3
|
+
Phase 1 surface:
|
|
4
|
+
.text (get/set)
|
|
5
|
+
.runs -> list[Run] (returns empty list in Phase 1 — Phase 2 will implement)
|
|
6
|
+
.style (get/set)
|
|
7
|
+
.alignment (get/set)
|
|
8
|
+
.add_run(text, bold=None, italic=None, ...) -> Run
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from docx._batching import run_sync
|
|
16
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from docx.client import Session
|
|
20
|
+
from docx.text.run import Run
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Paragraph:
|
|
24
|
+
"""A paragraph block in a Word document."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, *, session: "Session", node_id: str) -> None:
|
|
27
|
+
self._session: "Session" = session
|
|
28
|
+
self._node_id: str = node_id
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def text(self) -> str:
|
|
32
|
+
"""Return the plain-text content of this paragraph."""
|
|
33
|
+
block_obj: object = run_sync(
|
|
34
|
+
self._session.doc.blocks.get({"nodeId": self._node_id}),
|
|
35
|
+
)
|
|
36
|
+
if not isinstance(block_obj, dict):
|
|
37
|
+
return ""
|
|
38
|
+
return str(block_obj.get("text", ""))
|
|
39
|
+
|
|
40
|
+
@text.setter
|
|
41
|
+
def text(self, value: str) -> None:
|
|
42
|
+
"""Replace the entire text content of this paragraph."""
|
|
43
|
+
run_sync(
|
|
44
|
+
self._session.doc.blocks.update_text(
|
|
45
|
+
{"nodeId": self._node_id, "text": value},
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def runs(self) -> list["Run"]:
|
|
51
|
+
"""Return the runs in this paragraph.
|
|
52
|
+
|
|
53
|
+
Phase 1: returns an empty list. Superdoc's block model doesn't
|
|
54
|
+
expose a runs accessor on individual paragraphs in the current
|
|
55
|
+
SDK version; iterating them would require a custom JSON walker.
|
|
56
|
+
Phase 2 will implement this.
|
|
57
|
+
"""
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def style(self) -> str | None:
|
|
62
|
+
block_obj: object = run_sync(
|
|
63
|
+
self._session.doc.blocks.get({"nodeId": self._node_id}),
|
|
64
|
+
)
|
|
65
|
+
if not isinstance(block_obj, dict):
|
|
66
|
+
return None
|
|
67
|
+
style_id: str = str(block_obj.get("styleId", ""))
|
|
68
|
+
return style_id or None
|
|
69
|
+
|
|
70
|
+
@style.setter
|
|
71
|
+
def style(self, value: str | None) -> None:
|
|
72
|
+
run_sync(
|
|
73
|
+
self._session.doc.blocks.set_style(
|
|
74
|
+
{"nodeId": self._node_id, "styleId": value or "Normal"},
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def alignment(self) -> "WD_ALIGN_PARAGRAPH | None":
|
|
80
|
+
block_obj: object = run_sync(
|
|
81
|
+
self._session.doc.blocks.get({"nodeId": self._node_id}),
|
|
82
|
+
)
|
|
83
|
+
if not isinstance(block_obj, dict):
|
|
84
|
+
return None
|
|
85
|
+
raw: str = str(block_obj.get("alignment", ""))
|
|
86
|
+
return WD_ALIGN_PARAGRAPH.from_superdoc(raw) if raw else None
|
|
87
|
+
|
|
88
|
+
@alignment.setter
|
|
89
|
+
def alignment(self, value: "WD_ALIGN_PARAGRAPH | None") -> None:
|
|
90
|
+
sd_value: str = (
|
|
91
|
+
value.to_superdoc() if value is not None else "left"
|
|
92
|
+
)
|
|
93
|
+
run_sync(
|
|
94
|
+
self._session.doc.format.set_alignment(
|
|
95
|
+
{
|
|
96
|
+
"target": {"kind": "block", "nodeId": self._node_id},
|
|
97
|
+
"value": sd_value,
|
|
98
|
+
},
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def add_run(
|
|
103
|
+
self,
|
|
104
|
+
text: str = "",
|
|
105
|
+
style: str | None = None, # noqa: ARG002
|
|
106
|
+
) -> "Run":
|
|
107
|
+
"""Append a run to this paragraph and return it.
|
|
108
|
+
|
|
109
|
+
Strategy: append text at the end of the paragraph, then return
|
|
110
|
+
a Run proxy targeting that character range.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
text: The text content of the new run.
|
|
114
|
+
style: Ignored in Phase 1 (python-docx parity — accepts the
|
|
115
|
+
kwarg but has no-op behavior until Superdoc exposes
|
|
116
|
+
run-style references).
|
|
117
|
+
"""
|
|
118
|
+
from docx.text.run import Run
|
|
119
|
+
|
|
120
|
+
# Get current length to compute range start
|
|
121
|
+
block_obj: object = run_sync(
|
|
122
|
+
self._session.doc.blocks.get({"nodeId": self._node_id}),
|
|
123
|
+
)
|
|
124
|
+
current_text: str = (
|
|
125
|
+
str(block_obj.get("text", "")) if isinstance(block_obj, dict) else ""
|
|
126
|
+
)
|
|
127
|
+
range_start: int = len(current_text)
|
|
128
|
+
|
|
129
|
+
# Append text to the block
|
|
130
|
+
run_sync(
|
|
131
|
+
self._session.doc.blocks.append_text(
|
|
132
|
+
{"nodeId": self._node_id, "text": text},
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
range_end: int = range_start + len(text)
|
|
136
|
+
|
|
137
|
+
return Run(
|
|
138
|
+
session=self._session,
|
|
139
|
+
block_id=self._node_id,
|
|
140
|
+
range_=(range_start, range_end),
|
|
141
|
+
)
|
docx/text/run.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Run and Font — matches python-docx's run.Run and text.font.Font."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from docx._batching import run_sync
|
|
8
|
+
from docx.shared import Pt, RGBColor
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from docx.client import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Run:
|
|
15
|
+
"""A text run — a contiguous span with uniform formatting."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
session: "Session",
|
|
21
|
+
block_id: str,
|
|
22
|
+
range_: tuple[int, int],
|
|
23
|
+
) -> None:
|
|
24
|
+
self._session: "Session" = session
|
|
25
|
+
self._block_id: str = block_id
|
|
26
|
+
self._range: tuple[int, int] = range_
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def _text_range(self) -> dict:
|
|
30
|
+
return {
|
|
31
|
+
"kind": "text",
|
|
32
|
+
"blockId": self._block_id,
|
|
33
|
+
"range": {"start": self._range[0], "end": self._range[1]},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def text(self) -> str:
|
|
38
|
+
block_obj: object = run_sync(
|
|
39
|
+
self._session.doc.blocks.get({"nodeId": self._block_id}),
|
|
40
|
+
)
|
|
41
|
+
full: str = (
|
|
42
|
+
str(block_obj.get("text", "")) if isinstance(block_obj, dict) else ""
|
|
43
|
+
)
|
|
44
|
+
return full[self._range[0] : self._range[1]]
|
|
45
|
+
|
|
46
|
+
@text.setter
|
|
47
|
+
def text(self, value: str) -> None:
|
|
48
|
+
run_sync(
|
|
49
|
+
self._session.doc.replace(
|
|
50
|
+
{"target": self._text_range, "text": value},
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
new_end: int = self._range[0] + len(value)
|
|
54
|
+
self._range = (self._range[0], new_end)
|
|
55
|
+
|
|
56
|
+
def _apply_inline(self, **kwargs: object) -> None:
|
|
57
|
+
run_sync(
|
|
58
|
+
self._session.doc.format.apply(
|
|
59
|
+
{"target": self._text_range, "inline": kwargs},
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def bold(self) -> bool | None:
|
|
65
|
+
return self._get_inline_bool("bold")
|
|
66
|
+
|
|
67
|
+
@bold.setter
|
|
68
|
+
def bold(self, value: bool | None) -> None:
|
|
69
|
+
if value is None:
|
|
70
|
+
return
|
|
71
|
+
self._apply_inline(bold=value)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def italic(self) -> bool | None:
|
|
75
|
+
return self._get_inline_bool("italic")
|
|
76
|
+
|
|
77
|
+
@italic.setter
|
|
78
|
+
def italic(self, value: bool | None) -> None:
|
|
79
|
+
if value is None:
|
|
80
|
+
return
|
|
81
|
+
self._apply_inline(italic=value)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def underline(self) -> bool | None:
|
|
85
|
+
return self._get_inline_bool("underline")
|
|
86
|
+
|
|
87
|
+
@underline.setter
|
|
88
|
+
def underline(self, value: bool | None) -> None:
|
|
89
|
+
if value is None:
|
|
90
|
+
return
|
|
91
|
+
self._apply_inline(underline=value)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def font(self) -> "Font":
|
|
95
|
+
return Font(self)
|
|
96
|
+
|
|
97
|
+
def _get_inline_bool(self, name: str) -> bool | None:
|
|
98
|
+
block_obj: object = run_sync(
|
|
99
|
+
self._session.doc.blocks.get({"nodeId": self._block_id}),
|
|
100
|
+
)
|
|
101
|
+
if not isinstance(block_obj, dict):
|
|
102
|
+
return None
|
|
103
|
+
inline_obj: object = block_obj.get("inline", {})
|
|
104
|
+
if not isinstance(inline_obj, dict):
|
|
105
|
+
return None
|
|
106
|
+
val: object = inline_obj.get(name)
|
|
107
|
+
return val if isinstance(val, bool) else None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Font:
|
|
111
|
+
"""Font properties of a Run.
|
|
112
|
+
|
|
113
|
+
Accessed via `run.font`; do not instantiate directly.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, run: Run) -> None:
|
|
117
|
+
self._run: Run = run
|
|
118
|
+
|
|
119
|
+
def _get_inline(self) -> dict:
|
|
120
|
+
block_obj: object = run_sync(
|
|
121
|
+
self._run._session.doc.blocks.get({"nodeId": self._run._block_id}),
|
|
122
|
+
)
|
|
123
|
+
if not isinstance(block_obj, dict):
|
|
124
|
+
return {}
|
|
125
|
+
inline_obj: object = block_obj.get("inline", {})
|
|
126
|
+
return inline_obj if isinstance(inline_obj, dict) else {}
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def name(self) -> str | None:
|
|
130
|
+
inline: dict = self._get_inline()
|
|
131
|
+
value: str = str(inline.get("fontFamily", ""))
|
|
132
|
+
return value or None
|
|
133
|
+
|
|
134
|
+
@name.setter
|
|
135
|
+
def name(self, value: str | None) -> None:
|
|
136
|
+
if value is None:
|
|
137
|
+
return
|
|
138
|
+
self._run._apply_inline(fontFamily=value)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def size(self) -> "Pt | None":
|
|
142
|
+
inline: dict = self._get_inline()
|
|
143
|
+
sz: object = inline.get("fontSize")
|
|
144
|
+
if isinstance(sz, (int, float)):
|
|
145
|
+
return Pt(float(sz))
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
@size.setter
|
|
149
|
+
def size(self, value: "Pt | int | None") -> None:
|
|
150
|
+
if value is None:
|
|
151
|
+
return
|
|
152
|
+
pt_value: float = (
|
|
153
|
+
float(value.pt) if hasattr(value, "pt") else float(value)
|
|
154
|
+
)
|
|
155
|
+
self._run._apply_inline(fontSize=pt_value)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def color(self) -> "_ColorProxy":
|
|
159
|
+
return _ColorProxy(self._run)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _ColorProxy:
|
|
163
|
+
"""Matches python-docx's `run.font.color` property, which returns a
|
|
164
|
+
ColorFormat object with `.rgb` get/set.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, run: Run) -> None:
|
|
168
|
+
self._run: Run = run
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def rgb(self) -> "RGBColor | None":
|
|
172
|
+
block_obj: object = run_sync(
|
|
173
|
+
self._run._session.doc.blocks.get({"nodeId": self._run._block_id}),
|
|
174
|
+
)
|
|
175
|
+
if not isinstance(block_obj, dict):
|
|
176
|
+
return None
|
|
177
|
+
inline_obj: object = block_obj.get("inline", {})
|
|
178
|
+
if not isinstance(inline_obj, dict):
|
|
179
|
+
return None
|
|
180
|
+
hex_: str = str(inline_obj.get("color", ""))
|
|
181
|
+
return RGBColor.from_string(hex_) if hex_ else None
|
|
182
|
+
|
|
183
|
+
@rgb.setter
|
|
184
|
+
def rgb(self, value: "RGBColor | None") -> None:
|
|
185
|
+
if value is None:
|
|
186
|
+
return
|
|
187
|
+
self._run._apply_inline(color=str(value))
|
docx/typing.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Internal TypedDicts shared across the SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UserInfo(TypedDict):
|
|
9
|
+
"""Identity info passed to AsyncSuperDocClient for activity log attribution."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
email: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TextRange(TypedDict):
|
|
16
|
+
"""A Superdoc text range target.
|
|
17
|
+
|
|
18
|
+
Used in format.apply, replace, hyperlinks.wrap, etc.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
kind: str
|
|
22
|
+
blockId: str
|
|
23
|
+
range: "CharRange"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CharRange(TypedDict):
|
|
27
|
+
"""Start/end character offsets within a block."""
|
|
28
|
+
|
|
29
|
+
start: int
|
|
30
|
+
end: int
|