athena-python-docx 0.1.3__tar.gz → 0.1.6__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.3 → athena_python_docx-0.1.6}/PKG-INFO +1 -1
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/__init__.py +1 -1
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/document.py +114 -94
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/table.py +77 -63
- athena_python_docx-0.1.6/docx/text/paragraph.py +152 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/text/run.py +41 -56
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/pyproject.toml +1 -1
- athena_python_docx-0.1.6/tests/fidelity/README.md +71 -0
- athena_python_docx-0.1.6/tests/fidelity/__init__.py +14 -0
- athena_python_docx-0.1.6/tests/fidelity/cases.py +155 -0
- athena_python_docx-0.1.6/tests/fidelity/extract.py +259 -0
- athena_python_docx-0.1.6/tests/fidelity/runner.py +453 -0
- athena_python_docx-0.1.3/docx/text/paragraph.py +0 -141
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/.gitignore +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/CLAUDE.md +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/README.md +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/_batching.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/api.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/client.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/enum/table.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/enum/text.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/errors.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/shared.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/typing.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/scripts/publish.sh +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/tests/__init__.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/tests/conftest.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/tests/test_commands.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/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.1.6
|
|
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>
|
|
@@ -51,19 +51,37 @@ class Document:
|
|
|
51
51
|
def paragraphs(self) -> list["Paragraph"]:
|
|
52
52
|
"""Return all paragraphs in document order.
|
|
53
53
|
|
|
54
|
-
python-docx returns a live list; we return a snapshot.
|
|
55
|
-
|
|
54
|
+
python-docx returns a live list; we return a snapshot. For a
|
|
55
|
+
live-ish list, call this again.
|
|
56
56
|
"""
|
|
57
57
|
from docx.text.paragraph import Paragraph
|
|
58
58
|
|
|
59
59
|
self._ensure_open()
|
|
60
|
-
blocks: list[
|
|
61
|
-
|
|
60
|
+
# doc.blocks.list takes `nodeTypes: list[str]` (not `type: str`).
|
|
61
|
+
# Include "heading" too so iterating over paragraphs covers both —
|
|
62
|
+
# python-docx does not distinguish headings from paragraphs at the
|
|
63
|
+
# Document.paragraphs level either (they're all Paragraph instances).
|
|
64
|
+
raw: object = run_sync(
|
|
65
|
+
self._session.doc.blocks.list(
|
|
66
|
+
{"nodeTypes": ["paragraph", "heading"], "includeText": True},
|
|
67
|
+
),
|
|
62
68
|
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
# Real shape: {"blocks": [{"kind":"block","nodeType":..,"nodeId":..,"text":..}], ...}
|
|
70
|
+
if isinstance(raw, dict):
|
|
71
|
+
blocks_obj: object = raw.get("blocks", [])
|
|
72
|
+
blocks: list = blocks_obj if isinstance(blocks_obj, list) else []
|
|
73
|
+
elif isinstance(raw, list):
|
|
74
|
+
blocks = raw
|
|
75
|
+
else:
|
|
76
|
+
blocks = []
|
|
77
|
+
out: list["Paragraph"] = []
|
|
78
|
+
for b in blocks:
|
|
79
|
+
if not isinstance(b, dict):
|
|
80
|
+
continue
|
|
81
|
+
node_id: str = str(b.get("nodeId", ""))
|
|
82
|
+
if node_id:
|
|
83
|
+
out.append(Paragraph(session=self._session, node_id=node_id))
|
|
84
|
+
return out
|
|
67
85
|
|
|
68
86
|
@property
|
|
69
87
|
def tables(self) -> list["Table"]:
|
|
@@ -101,48 +119,29 @@ class Document:
|
|
|
101
119
|
|
|
102
120
|
self._ensure_open()
|
|
103
121
|
|
|
104
|
-
# Superdoc's
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
128
|
-
else:
|
|
129
|
-
md: str
|
|
130
|
-
if style and style.lower().startswith("heading"):
|
|
131
|
-
try:
|
|
132
|
-
level: int = int(style.rsplit(" ", 1)[-1])
|
|
133
|
-
except ValueError:
|
|
134
|
-
level = 1
|
|
135
|
-
md = f"{'#' * level} {text}\n\n"
|
|
136
|
-
else:
|
|
137
|
-
md = f"{text}\n\n"
|
|
138
|
-
|
|
139
|
-
result = run_sync(
|
|
140
|
-
self._session.doc.insert(
|
|
141
|
-
{"value": md, "type": "markdown"},
|
|
142
|
-
),
|
|
143
|
-
)
|
|
144
|
-
node_id = _extract_inserted_node_id(result, expected_type="paragraph")
|
|
145
|
-
|
|
122
|
+
# Use Superdoc's canonical primitive `doc.create.paragraph`. It
|
|
123
|
+
# accepts an `at` anchor and a `text` body; an empty `text` creates
|
|
124
|
+
# a truly-empty paragraph (no markdown-parser edge case).
|
|
125
|
+
#
|
|
126
|
+
# For heading styles we still dispatch to add_heading, which uses
|
|
127
|
+
# `doc.create.heading` (also canonical, takes `level`).
|
|
128
|
+
if style and style.lower().startswith("heading"):
|
|
129
|
+
try:
|
|
130
|
+
level: int = int(style.rsplit(" ", 1)[-1])
|
|
131
|
+
except ValueError:
|
|
132
|
+
level = 1
|
|
133
|
+
return self.add_heading(text=text, level=level)
|
|
134
|
+
if style and style.lower() == "title":
|
|
135
|
+
return self.add_heading(text=text, level=0)
|
|
136
|
+
|
|
137
|
+
params: dict = {
|
|
138
|
+
"text": text,
|
|
139
|
+
"at": {"kind": "documentEnd"},
|
|
140
|
+
}
|
|
141
|
+
result: dict = run_sync(
|
|
142
|
+
self._session.doc.create.paragraph(params),
|
|
143
|
+
)
|
|
144
|
+
node_id: str = _extract_inserted_node_id(result, expected_type="paragraph")
|
|
146
145
|
if not node_id:
|
|
147
146
|
raise RuntimeError(
|
|
148
147
|
f"Superdoc did not return a nodeId for add_paragraph: {result!r}",
|
|
@@ -154,13 +153,33 @@ class Document:
|
|
|
154
153
|
text: str = "",
|
|
155
154
|
level: int = 1,
|
|
156
155
|
) -> "Paragraph":
|
|
157
|
-
"""Append a heading
|
|
156
|
+
"""Append a heading via Superdoc's canonical `doc.create.heading`."""
|
|
157
|
+
from docx.text.paragraph import Paragraph
|
|
158
|
+
|
|
159
|
+
self._ensure_open()
|
|
158
160
|
if not 0 <= level <= 9:
|
|
159
161
|
raise ValidationError(
|
|
160
162
|
f"level must be in 0..9; got {level}",
|
|
161
163
|
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
|
|
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).
|
|
169
|
+
params: dict = {
|
|
170
|
+
"text": text,
|
|
171
|
+
"level": level if level >= 1 else 1,
|
|
172
|
+
"at": {"kind": "documentEnd"},
|
|
173
|
+
}
|
|
174
|
+
result: dict = run_sync(
|
|
175
|
+
self._session.doc.create.heading(params),
|
|
176
|
+
)
|
|
177
|
+
node_id: str = _extract_inserted_node_id(result, expected_type="paragraph")
|
|
178
|
+
if not node_id:
|
|
179
|
+
raise RuntimeError(
|
|
180
|
+
f"Superdoc did not return a nodeId for add_heading: {result!r}",
|
|
181
|
+
)
|
|
182
|
+
return Paragraph(session=self._session, node_id=node_id)
|
|
164
183
|
|
|
165
184
|
def add_table(
|
|
166
185
|
self,
|
|
@@ -168,7 +187,7 @@ class Document:
|
|
|
168
187
|
cols: int,
|
|
169
188
|
style: str | None = None,
|
|
170
189
|
) -> "Table":
|
|
171
|
-
"""Append a table with the given dimensions."""
|
|
190
|
+
"""Append a table with the given dimensions via `doc.create.table`."""
|
|
172
191
|
from docx.table import Table
|
|
173
192
|
|
|
174
193
|
self._ensure_open()
|
|
@@ -177,18 +196,18 @@ class Document:
|
|
|
177
196
|
f"rows and cols must be >= 1; got rows={rows} cols={cols}",
|
|
178
197
|
)
|
|
179
198
|
|
|
199
|
+
# The real param is `columns` (not `cols`). `at` is required to
|
|
200
|
+
# position the table; we always append to document end.
|
|
180
201
|
result: dict = run_sync(
|
|
181
202
|
self._session.doc.create.table(
|
|
182
|
-
{
|
|
203
|
+
{
|
|
204
|
+
"rows": rows,
|
|
205
|
+
"columns": cols,
|
|
206
|
+
"at": {"kind": "documentEnd"},
|
|
207
|
+
},
|
|
183
208
|
),
|
|
184
209
|
)
|
|
185
|
-
|
|
186
|
-
result.get("result", {}) if isinstance(result, dict) else {}
|
|
187
|
-
)
|
|
188
|
-
table_info: dict = (
|
|
189
|
-
result_data.get("table", {}) if isinstance(result_data, dict) else {}
|
|
190
|
-
)
|
|
191
|
-
node_id: str = str(table_info.get("nodeId", ""))
|
|
210
|
+
node_id: str = _extract_inserted_node_id(result, expected_type="table")
|
|
192
211
|
if not node_id:
|
|
193
212
|
raise RuntimeError(
|
|
194
213
|
f"Superdoc did not return a nodeId for add_table: {result!r}",
|
|
@@ -201,9 +220,7 @@ class Document:
|
|
|
201
220
|
),
|
|
202
221
|
)
|
|
203
222
|
# Re-fetch nodeId — set_style may rotate it. Prefer matching the
|
|
204
|
-
# original id
|
|
205
|
-
# don't cause us to pick up the wrong table. Only fall back to
|
|
206
|
-
# last-in-list (with a warning) when the original id is gone.
|
|
223
|
+
# original id; fall back to last-in-list with a warning.
|
|
207
224
|
refetch: dict = run_sync(
|
|
208
225
|
self._session.doc.find({"type": "table"}),
|
|
209
226
|
)
|
|
@@ -233,7 +250,7 @@ class Document:
|
|
|
233
250
|
width: "Emu | int | None" = None,
|
|
234
251
|
height: "Emu | int | None" = None,
|
|
235
252
|
) -> None:
|
|
236
|
-
"""Append an inline image
|
|
253
|
+
"""Append an inline image via `doc.create.image`."""
|
|
237
254
|
self._ensure_open()
|
|
238
255
|
|
|
239
256
|
image_bytes: bytes
|
|
@@ -259,9 +276,6 @@ class Document:
|
|
|
259
276
|
b64: str = base64.b64encode(image_bytes).decode("ascii")
|
|
260
277
|
data_uri: str = f"data:{content_type};base64,{b64}"
|
|
261
278
|
|
|
262
|
-
# Default 6-inch wide, proportional 4.5-inch tall at 96 DPI
|
|
263
|
-
# (python-docx uses image's intrinsic size; we don't have that here,
|
|
264
|
-
# so we default to a reasonable 6x4.5 inch rectangle)
|
|
265
279
|
w_px: float = _emu_to_px(width) if width is not None else 576.0
|
|
266
280
|
h_px: float = _emu_to_px(height) if height is not None else 432.0
|
|
267
281
|
|
|
@@ -276,13 +290,16 @@ class Document:
|
|
|
276
290
|
)
|
|
277
291
|
|
|
278
292
|
def add_page_break(self) -> None:
|
|
279
|
-
"""Append a page break at the end of the document.
|
|
293
|
+
"""Append a page break at the end of the document.
|
|
294
|
+
|
|
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).
|
|
298
|
+
"""
|
|
280
299
|
self._ensure_open()
|
|
281
|
-
# Use form-feed character; Superdoc's markdown parser converts
|
|
282
|
-
# this to a <w:br w:type="page"/> in export.
|
|
283
300
|
run_sync(
|
|
284
|
-
self._session.doc.
|
|
285
|
-
{"
|
|
301
|
+
self._session.doc.create.section_break(
|
|
302
|
+
{"at": {"kind": "documentEnd"}, "breakType": "nextPage"},
|
|
286
303
|
),
|
|
287
304
|
)
|
|
288
305
|
|
|
@@ -333,29 +350,32 @@ class Document:
|
|
|
333
350
|
# ---- Module-level helpers ----
|
|
334
351
|
|
|
335
352
|
def _extract_inserted_node_id(result: dict, *, expected_type: str) -> str:
|
|
336
|
-
"""Parse Superdoc
|
|
337
|
-
|
|
338
|
-
Superdoc rotates response shapes across versions; we probe in order
|
|
339
|
-
of "most current" first. Real observed shape (superdoc-sdk 1.6.0.dev29):
|
|
340
|
-
|
|
341
|
-
{
|
|
342
|
-
"receipt": {
|
|
343
|
-
"resolution": {
|
|
344
|
-
"target": {"kind": "text", "blockId": "<uuid>", "range": {...}}
|
|
345
|
-
}
|
|
346
|
-
},
|
|
347
|
-
...
|
|
348
|
-
}
|
|
353
|
+
"""Parse Superdoc create/insert response to find the new node's blockId.
|
|
349
354
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
355
|
+
Real observed shapes (superdoc-sdk 1.6.0.dev29):
|
|
356
|
+
|
|
357
|
+
doc.create.paragraph/heading/table returns
|
|
358
|
+
{"success": True, "<paragraph|heading|table>": {"nodeId": "..."}}
|
|
359
|
+
|
|
360
|
+
doc.insert (markdown/text) returns
|
|
361
|
+
{"receipt": {"resolution": {"target": {"blockId": "..."}}}, ...}
|
|
362
|
+
|
|
363
|
+
We probe create-shape first (nested by expected_type), then insert-shape.
|
|
364
|
+
Returns "" if nothing matches.
|
|
354
365
|
"""
|
|
355
366
|
if not isinstance(result, dict):
|
|
356
367
|
return ""
|
|
357
368
|
|
|
358
|
-
# Shape 1
|
|
369
|
+
# Shape 1 — doc.create.paragraph/heading/table/image: top-level key
|
|
370
|
+
# named by the result type holds `{kind, nodeType, nodeId}`.
|
|
371
|
+
for key in (expected_type, "paragraph", "heading", "table", "image"):
|
|
372
|
+
obj = result.get(key)
|
|
373
|
+
if isinstance(obj, dict):
|
|
374
|
+
node_id = obj.get("nodeId")
|
|
375
|
+
if isinstance(node_id, str) and node_id:
|
|
376
|
+
return node_id
|
|
377
|
+
|
|
378
|
+
# Shape 2 — doc.insert: receipt.resolution.target.blockId.
|
|
359
379
|
receipt: object = result.get("receipt")
|
|
360
380
|
if isinstance(receipt, dict):
|
|
361
381
|
resolution: object = receipt.get("resolution")
|
|
@@ -366,7 +386,7 @@ def _extract_inserted_node_id(result: dict, *, expected_type: str) -> str:
|
|
|
366
386
|
if block_id:
|
|
367
387
|
return block_id
|
|
368
388
|
|
|
369
|
-
# Shape
|
|
389
|
+
# Shape 3 — top-level blockId or target.blockId.
|
|
370
390
|
top_block: object = result.get("blockId")
|
|
371
391
|
if isinstance(top_block, str) and top_block:
|
|
372
392
|
return top_block
|
|
@@ -376,7 +396,7 @@ def _extract_inserted_node_id(result: dict, *, expected_type: str) -> str:
|
|
|
376
396
|
if tb:
|
|
377
397
|
return tb
|
|
378
398
|
|
|
379
|
-
# Shape
|
|
399
|
+
# Shape 4 (legacy): nested result.result.insertedNodes[] / nodes[].
|
|
380
400
|
data_obj: object = result.get("result", {})
|
|
381
401
|
data: dict = data_obj if isinstance(data_obj, dict) else {}
|
|
382
402
|
nodes_obj: object = data.get("insertedNodes", data.get("nodes", []))
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""Table, _Row, _Column, _Cell — python-docx parity.
|
|
2
2
|
|
|
3
|
-
Phase 1
|
|
4
|
-
Table.rows
|
|
5
|
-
Table.
|
|
6
|
-
Table.
|
|
7
|
-
|
|
8
|
-
_Cell.
|
|
3
|
+
Phase 1 supported:
|
|
4
|
+
Table.rows / columns — via doc.tables.get
|
|
5
|
+
Table.cell(r, c) — builds a _Cell proxy (lazy)
|
|
6
|
+
Table.add_row / add_column — via doc.tables.insert_row / insert_column
|
|
7
|
+
Table.style setter — via doc.tables.set_style
|
|
8
|
+
_Cell.text getter — via doc.tables.get_cells (filtered)
|
|
9
|
+
_Cell.merge(other) — via doc.tables.merge_cells
|
|
10
|
+
|
|
11
|
+
Phase 1 NOT supported (NotImplementedError):
|
|
12
|
+
Table.style getter — via doc.tables.getStyles/getProperties
|
|
13
|
+
_Cell.text setter — needs find+replace-in-cell design
|
|
9
14
|
"""
|
|
10
15
|
|
|
11
16
|
from __future__ import annotations
|
|
@@ -20,6 +25,12 @@ if TYPE_CHECKING:
|
|
|
20
25
|
from docx.client import Session
|
|
21
26
|
|
|
22
27
|
|
|
28
|
+
_STUB_SUFFIX = (
|
|
29
|
+
" — not yet mapped to a Superdoc SDK primitive; see "
|
|
30
|
+
"docx-studio/python-sdk/CLAUDE.md for Phase 2 scope."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
23
34
|
def _log_warn(msg: str) -> None:
|
|
24
35
|
print(f"[docx-sdk] WARN: {msg}", file=sys.stderr)
|
|
25
36
|
|
|
@@ -30,26 +41,28 @@ class Table:
|
|
|
30
41
|
self._node_id: str = node_id
|
|
31
42
|
|
|
32
43
|
def _fresh_node_id(self) -> str:
|
|
33
|
-
"""Re-find this table by its
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
find_result: dict = run_sync(
|
|
44
|
+
"""Re-find this table by its original nodeId first; warn+last-in-list
|
|
45
|
+
as a last resort (set_style etc. can rotate ids)."""
|
|
46
|
+
find_result: object = run_sync(
|
|
37
47
|
self._session.doc.find({"type": "table"}),
|
|
38
48
|
)
|
|
39
|
-
|
|
49
|
+
items_obj: object = (
|
|
50
|
+
find_result.get("items", []) if isinstance(find_result, dict) else []
|
|
51
|
+
)
|
|
52
|
+
items: list = items_obj if isinstance(items_obj, list) else []
|
|
40
53
|
for item in items:
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
if not isinstance(item, dict):
|
|
55
|
+
continue
|
|
56
|
+
addr = item.get("address", {})
|
|
57
|
+
if isinstance(addr, dict) and addr.get("nodeId") == self._node_id:
|
|
43
58
|
return self._node_id
|
|
44
|
-
# Not found by exact id — trust the LAST table as ours, with a warning
|
|
45
59
|
if items:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
60
|
+
last = items[-1] if isinstance(items[-1], dict) else {}
|
|
61
|
+
last_addr = last.get("address", {}) if isinstance(last, dict) else {}
|
|
49
62
|
new_id: str = str(last_addr.get("nodeId", ""))
|
|
50
63
|
if new_id:
|
|
51
64
|
_log_warn(
|
|
52
|
-
f"
|
|
65
|
+
f"table nodeId rotated: {self._node_id} -> {new_id}",
|
|
53
66
|
)
|
|
54
67
|
self._node_id = new_id
|
|
55
68
|
return self._node_id
|
|
@@ -57,21 +70,29 @@ class Table:
|
|
|
57
70
|
@property
|
|
58
71
|
def rows(self) -> list["_Row"]:
|
|
59
72
|
nid: str = self._fresh_node_id()
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
info: object = run_sync(self._session.doc.tables.get({"nodeId": nid}))
|
|
74
|
+
info_dict: dict = info if isinstance(info, dict) else {}
|
|
75
|
+
# doc.tables.get may return row count at various paths
|
|
76
|
+
row_count: int = int(
|
|
77
|
+
info_dict.get("rows")
|
|
78
|
+
or info_dict.get("rowCount")
|
|
79
|
+
or (info_dict.get("table", {}) or {}).get("rows")
|
|
80
|
+
or 0,
|
|
62
81
|
)
|
|
63
|
-
info: dict = info_obj if isinstance(info_obj, dict) else {}
|
|
64
|
-
row_count: int = int(info.get("rows", 0))
|
|
65
82
|
return [_Row(table=self, index=i) for i in range(row_count)]
|
|
66
83
|
|
|
67
84
|
@property
|
|
68
85
|
def columns(self) -> list["_Column"]:
|
|
69
86
|
nid: str = self._fresh_node_id()
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
info: object = run_sync(self._session.doc.tables.get({"nodeId": nid}))
|
|
88
|
+
info_dict: dict = info if isinstance(info, dict) else {}
|
|
89
|
+
col_count: int = int(
|
|
90
|
+
info_dict.get("columns")
|
|
91
|
+
or info_dict.get("cols")
|
|
92
|
+
or info_dict.get("columnCount")
|
|
93
|
+
or (info_dict.get("table", {}) or {}).get("columns")
|
|
94
|
+
or 0,
|
|
72
95
|
)
|
|
73
|
-
info: dict = info_obj if isinstance(info_obj, dict) else {}
|
|
74
|
-
col_count: int = int(info.get("cols", 0))
|
|
75
96
|
return [_Column(table=self, index=j) for j in range(col_count)]
|
|
76
97
|
|
|
77
98
|
def cell(self, row_idx: int, col_idx: int) -> "_Cell":
|
|
@@ -81,15 +102,15 @@ class Table:
|
|
|
81
102
|
nid: str = self._fresh_node_id()
|
|
82
103
|
row_count: int = len(self.rows)
|
|
83
104
|
if row_count < 1:
|
|
84
|
-
raise ValidationError(
|
|
85
|
-
|
|
86
|
-
)
|
|
105
|
+
raise ValidationError(f"Cannot add row to empty table {nid}")
|
|
106
|
+
# Real params: nodeId, rowIndex, position, count
|
|
87
107
|
run_sync(
|
|
88
108
|
self._session.doc.tables.insert_row(
|
|
89
109
|
{
|
|
90
|
-
"
|
|
110
|
+
"nodeId": nid,
|
|
91
111
|
"rowIndex": row_count - 1,
|
|
92
112
|
"position": "below",
|
|
113
|
+
"count": 1,
|
|
93
114
|
},
|
|
94
115
|
),
|
|
95
116
|
)
|
|
@@ -99,15 +120,14 @@ class Table:
|
|
|
99
120
|
nid: str = self._fresh_node_id()
|
|
100
121
|
col_count: int = len(self.columns)
|
|
101
122
|
if col_count < 1:
|
|
102
|
-
raise ValidationError(
|
|
103
|
-
f"Cannot add column to empty table {nid}",
|
|
104
|
-
)
|
|
123
|
+
raise ValidationError(f"Cannot add column to empty table {nid}")
|
|
105
124
|
run_sync(
|
|
106
125
|
self._session.doc.tables.insert_column(
|
|
107
126
|
{
|
|
108
|
-
"
|
|
127
|
+
"nodeId": nid,
|
|
109
128
|
"columnIndex": col_count - 1,
|
|
110
129
|
"position": "right",
|
|
130
|
+
"count": 1,
|
|
111
131
|
},
|
|
112
132
|
),
|
|
113
133
|
)
|
|
@@ -115,13 +135,7 @@ class Table:
|
|
|
115
135
|
|
|
116
136
|
@property
|
|
117
137
|
def style(self) -> str | None:
|
|
118
|
-
|
|
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
|
|
138
|
+
raise NotImplementedError("Table.style getter" + _STUB_SUFFIX)
|
|
125
139
|
|
|
126
140
|
@style.setter
|
|
127
141
|
def style(self, value: str | None) -> None:
|
|
@@ -170,28 +184,30 @@ class _Cell:
|
|
|
170
184
|
@property
|
|
171
185
|
def text(self) -> str:
|
|
172
186
|
nid: str = self._table._fresh_node_id()
|
|
173
|
-
|
|
174
|
-
self._table._session.doc.tables.
|
|
175
|
-
{
|
|
187
|
+
result: object = run_sync(
|
|
188
|
+
self._table._session.doc.tables.get_cells(
|
|
189
|
+
{
|
|
190
|
+
"nodeId": nid,
|
|
191
|
+
"rowIndex": self._row,
|
|
192
|
+
"columnIndex": self._col,
|
|
193
|
+
},
|
|
176
194
|
),
|
|
177
195
|
)
|
|
178
|
-
if not isinstance(
|
|
196
|
+
if not isinstance(result, dict):
|
|
197
|
+
return ""
|
|
198
|
+
cells_obj: object = result.get("cells", result.get("items", []))
|
|
199
|
+
cells: list = cells_obj if isinstance(cells_obj, list) else []
|
|
200
|
+
if not cells or not isinstance(cells[0], dict):
|
|
179
201
|
return ""
|
|
180
|
-
|
|
202
|
+
first = cells[0]
|
|
203
|
+
text: object = first.get("text")
|
|
204
|
+
if isinstance(text, str):
|
|
205
|
+
return text
|
|
206
|
+
return ""
|
|
181
207
|
|
|
182
208
|
@text.setter
|
|
183
|
-
def text(self, value: str) -> None:
|
|
184
|
-
|
|
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
|
-
)
|
|
209
|
+
def text(self, value: str) -> None: # noqa: ARG002
|
|
210
|
+
raise NotImplementedError("_Cell.text setter" + _STUB_SUFFIX)
|
|
195
211
|
|
|
196
212
|
def merge(self, other: "_Cell") -> "_Cell":
|
|
197
213
|
if other._table is not self._table:
|
|
@@ -202,11 +218,9 @@ class _Cell:
|
|
|
202
218
|
run_sync(
|
|
203
219
|
self._table._session.doc.tables.merge_cells(
|
|
204
220
|
{
|
|
205
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"endRow": other._row,
|
|
209
|
-
"endCol": other._col,
|
|
221
|
+
"nodeId": nid,
|
|
222
|
+
"start": {"rowIndex": self._row, "columnIndex": self._col},
|
|
223
|
+
"end": {"rowIndex": other._row, "columnIndex": other._col},
|
|
210
224
|
},
|
|
211
225
|
),
|
|
212
226
|
)
|