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.
Files changed (32) hide show
  1. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/PKG-INFO +1 -1
  2. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/__init__.py +1 -1
  3. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/document.py +114 -94
  4. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/table.py +77 -63
  5. athena_python_docx-0.1.6/docx/text/paragraph.py +152 -0
  6. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/text/run.py +41 -56
  7. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/pyproject.toml +1 -1
  8. athena_python_docx-0.1.6/tests/fidelity/README.md +71 -0
  9. athena_python_docx-0.1.6/tests/fidelity/__init__.py +14 -0
  10. athena_python_docx-0.1.6/tests/fidelity/cases.py +155 -0
  11. athena_python_docx-0.1.6/tests/fidelity/extract.py +259 -0
  12. athena_python_docx-0.1.6/tests/fidelity/runner.py +453 -0
  13. athena_python_docx-0.1.3/docx/text/paragraph.py +0 -141
  14. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/.gitignore +0 -0
  15. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/CLAUDE.md +0 -0
  16. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/README.md +0 -0
  17. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/_batching.py +0 -0
  18. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/api.py +0 -0
  19. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/client.py +0 -0
  20. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/enum/__init__.py +0 -0
  21. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/enum/table.py +0 -0
  22. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/enum/text.py +0 -0
  23. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/errors.py +0 -0
  24. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/shared.py +0 -0
  25. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/text/__init__.py +0 -0
  26. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/docx/typing.py +0 -0
  27. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/scripts/publish.sh +0 -0
  28. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/tests/__init__.py +0 -0
  29. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/tests/conftest.py +0 -0
  30. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/tests/test_commands.py +0 -0
  31. {athena_python_docx-0.1.3 → athena_python_docx-0.1.6}/tests/test_python_docx_api_parity.py +0 -0
  32. {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
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>
@@ -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.3"
9
+ __version__ = "0.1.6"
10
10
 
11
11
  from docx.api import Document
12
12
 
@@ -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
- For a live-ish list, call this again.
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[dict] = run_sync(
61
- self._session.doc.blocks.list({"type": "paragraph"}),
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
- return [
64
- Paragraph(session=self._session, node_id=b["nodeId"])
65
- for b in blocks
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 markdown parser rejects empty input with
105
- # "Markdown produced no content to insert." For an empty paragraph
106
- # (python-docx's `doc.add_paragraph()` with no args), we use the
107
- # text-type insert with a single space and immediately clear it —
108
- # same pattern as `agora/web/api/super_docs/superdoc_write_utils.py`
109
- # around line 1085.
110
- result: dict
111
- node_id: str
112
- if not text and not (style and style.lower().startswith("heading")):
113
- result = run_sync(
114
- self._session.doc.insert(
115
- {"value": " ", "type": "text"},
116
- ),
117
- )
118
- node_id = _extract_inserted_node_id(result, expected_type="paragraph")
119
- if node_id:
120
- # Clear the placeholder space so the paragraph reads as empty
121
- # (matches python-docx semantics: a freshly-added blank paragraph
122
- # has empty `.text`).
123
- run_sync(
124
- self._session.doc.blocks.update_text(
125
- {"nodeId": node_id, "text": ""},
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 (convenience over add_paragraph)."""
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
- style: str = "Title" if level == 0 else f"Heading {level}"
163
- return self.add_paragraph(text=text, style=style)
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
- {"rows": rows, "cols": cols},
203
+ {
204
+ "rows": rows,
205
+ "columns": cols,
206
+ "at": {"kind": "documentEnd"},
207
+ },
183
208
  ),
184
209
  )
185
- result_data: dict = (
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 so concurrent table insertions from other editors
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 at the end of the document."""
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.insert(
285
- {"value": "\f", "type": "text"},
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's insert() response to find the relevant blockId.
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
- Older/alternate shapes we also accept:
351
- - top-level ``blockId``
352
- - nested ``result.insertedNodes[].nodeId`` (when typed = expected_type)
353
- - last element of ``result.nodes``
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 (current): receipt.resolution.target.blockId
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 2: top-level blockId or target.blockId
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 3 (legacy): nested result.result.insertedNodes[] / nodes[]
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 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)
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 position. Superdoc rotates nodeIds
34
- after style/option changes (see superdoc_write_utils.py:942-948).
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
- items: list[dict] = find_result.get("items", [])
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
- addr: dict = item.get("address", {}) if isinstance(item, dict) else {}
42
- if addr.get("nodeId") == self._node_id:
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
- last_addr: dict = (
47
- items[-1].get("address", {}) if isinstance(items[-1], dict) else {}
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"Table nodeId rotated: {self._node_id} -> {new_id}",
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
- info_obj: object = run_sync(
61
- self._session.doc.tables.get({"nodeId": nid}),
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
- info_obj: object = run_sync(
71
- self._session.doc.tables.get({"nodeId": nid}),
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
- f"Cannot add row to empty table {nid}",
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
- "tableNodeId": nid,
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
- "tableNodeId": nid,
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
- 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
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
- cell_obj: object = run_sync(
174
- self._table._session.doc.tables.get_cell(
175
- {"tableNodeId": nid, "row": self._row, "col": self._col},
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(cell_obj, dict):
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
- return str(cell_obj.get("text", ""))
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
- 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
- )
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
- "tableNodeId": nid,
206
- "startRow": self._row,
207
- "startCol": self._col,
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
  )