athena-python-docx 0.1.0__tar.gz → 0.1.2__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 (26) hide show
  1. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/PKG-INFO +1 -1
  2. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/__init__.py +1 -1
  3. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/document.py +83 -17
  4. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/pyproject.toml +1 -1
  5. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/.gitignore +0 -0
  6. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/CLAUDE.md +0 -0
  7. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/README.md +0 -0
  8. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/_batching.py +0 -0
  9. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/api.py +0 -0
  10. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/client.py +0 -0
  11. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/enum/__init__.py +0 -0
  12. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/enum/table.py +0 -0
  13. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/enum/text.py +0 -0
  14. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/errors.py +0 -0
  15. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/shared.py +0 -0
  16. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/table.py +0 -0
  17. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/text/__init__.py +0 -0
  18. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/text/paragraph.py +0 -0
  19. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/text/run.py +0 -0
  20. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/docx/typing.py +0 -0
  21. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/scripts/publish.sh +0 -0
  22. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/tests/__init__.py +0 -0
  23. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/tests/conftest.py +0 -0
  24. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/tests/test_commands.py +0 -0
  25. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/tests/test_python_docx_api_parity.py +0 -0
  26. {athena_python_docx-0.1.0 → athena_python_docx-0.1.2}/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.0
3
+ Version: 0.1.2
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.0"
9
+ __version__ = "0.1.2"
10
10
 
11
11
  from docx.api import Document
12
12
 
@@ -100,22 +100,49 @@ class Document:
100
100
  from docx.text.paragraph import Paragraph
101
101
 
102
102
  self._ensure_open()
103
- md: str
104
- if style and style.lower().startswith("heading"):
105
- try:
106
- level: int = int(style.rsplit(" ", 1)[-1])
107
- except ValueError:
108
- level = 1
109
- md = f"{'#' * level} {text}\n\n"
103
+
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
+ )
110
128
  else:
111
- md = f"{text}\n\n"
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")
112
145
 
113
- result: dict = run_sync(
114
- self._session.doc.insert(
115
- {"value": md, "type": "markdown"},
116
- ),
117
- )
118
- node_id: str = _extract_inserted_node_id(result, expected_type="paragraph")
119
146
  if not node_id:
120
147
  raise RuntimeError(
121
148
  f"Superdoc did not return a nodeId for add_paragraph: {result!r}",
@@ -306,12 +333,50 @@ class Document:
306
333
  # ---- Module-level helpers ----
307
334
 
308
335
  def _extract_inserted_node_id(result: dict, *, expected_type: str) -> str:
309
- """Parse Superdoc's insert() response to find the new nodeId.
310
-
311
- Superdoc rotates response shape across versions. Defensively probe.
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
+ }
349
+
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``
312
354
  """
313
355
  if not isinstance(result, dict):
314
356
  return ""
357
+
358
+ # Shape 1 (current): receipt.resolution.target.blockId
359
+ receipt: object = result.get("receipt")
360
+ if isinstance(receipt, dict):
361
+ resolution: object = receipt.get("resolution")
362
+ if isinstance(resolution, dict):
363
+ target: object = resolution.get("target")
364
+ if isinstance(target, dict):
365
+ block_id: str = str(target.get("blockId", ""))
366
+ if block_id:
367
+ return block_id
368
+
369
+ # Shape 2: top-level blockId or target.blockId
370
+ top_block: object = result.get("blockId")
371
+ if isinstance(top_block, str) and top_block:
372
+ return top_block
373
+ top_target: object = result.get("target")
374
+ if isinstance(top_target, dict):
375
+ tb: str = str(top_target.get("blockId", ""))
376
+ if tb:
377
+ return tb
378
+
379
+ # Shape 3 (legacy): nested result.result.insertedNodes[] / nodes[]
315
380
  data_obj: object = result.get("result", {})
316
381
  data: dict = data_obj if isinstance(data_obj, dict) else {}
317
382
  nodes_obj: object = data.get("insertedNodes", data.get("nodes", []))
@@ -323,6 +388,7 @@ def _extract_inserted_node_id(result: dict, *, expected_type: str) -> str:
323
388
  return str(node.get("nodeId", ""))
324
389
  if nodes and isinstance(nodes[-1], dict):
325
390
  return str(nodes[-1].get("nodeId", ""))
391
+
326
392
  return ""
327
393
 
328
394
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "athena-python-docx"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Drop-in replacement for python-docx that connects to Athena's Superdoc/Keryx collaborative document stack"
9
9
  readme = "README.md"
10
10
  license = "MIT"