roborean-documents-docx 0.1.2__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.
@@ -0,0 +1,172 @@
1
+ """DOCX document driver using docxtpl and python-docx."""
2
+
3
+ import io
4
+ import logging
5
+ from dataclasses import dataclass, field
6
+ from datetime import UTC, datetime
7
+ from typing import Any, Mapping
8
+
9
+ from docx import Document
10
+ from docxtpl import DocxTemplate
11
+ from pydantic import TypeAdapter
12
+ from roborean_documents_base.capabilities import assert_op_allowed
13
+ from roborean_documents_base.errors import DriverError
14
+ from roborean_documents_base.resolve_values import public_literal_value
15
+ from roborean_spec import (
16
+ DocumentDriverManifest,
17
+ DocumentOperation,
18
+ DocumentPreview,
19
+ TemplateManifest,
20
+ WorkspaceValue,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @dataclass
27
+ class DocxSession:
28
+ """Working DOCX document session."""
29
+
30
+ document_id: str
31
+ driver_id: str
32
+ document: Any
33
+ ops_applied: list[dict[str, Any]] = field(default_factory=list)
34
+ context: dict[str, Any] = field(default_factory=dict)
35
+
36
+
37
+ class DocxDocumentDriver:
38
+ """Word document driver with flow ops and named slots."""
39
+
40
+ driver_id = "roborean.docx"
41
+ manifest = DocumentDriverManifest(
42
+ driverId="roborean.docx",
43
+ version="0.3.0",
44
+ irFamily="flow",
45
+ capabilities=[
46
+ "set_metadata",
47
+ "replace_named_value",
48
+ "flow.insert_paragraph",
49
+ "flow.insert_heading",
50
+ "finalize",
51
+ ],
52
+ supportsPreview=True,
53
+ supportsBrowserExecution=False,
54
+ supportsDiff=True,
55
+ requiresBackend=True,
56
+ templateMediaTypes=[
57
+ "application/vnd.openxmlformats-officedocument"
58
+ ".wordprocessingml.document"
59
+ ],
60
+ )
61
+
62
+ def __init__(self) -> None:
63
+ """Initialize empty template state."""
64
+ self._template: bytes | None = None
65
+ self._manifest: TemplateManifest | None = None
66
+
67
+ def load_template(self, template_ref, *, store, manifest) -> None:
68
+ """Load .docx template bytes."""
69
+ self._template = store.load_bytes(template_ref)
70
+ self._manifest = manifest
71
+
72
+ def begin_session(
73
+ self, workspace, metadata: Mapping[str, Any]
74
+ ) -> DocxSession:
75
+ """Render docxtpl context then open a python-docx document."""
76
+ assert self._template is not None
77
+ tpl = DocxTemplate(io.BytesIO(self._template))
78
+ context: dict[str, Any] = {}
79
+ if self._manifest is not None:
80
+ for key in self._manifest.required_inputs:
81
+ if key in workspace.values:
82
+ try:
83
+ context[key] = public_literal_value(
84
+ workspace.values[key]
85
+ )
86
+ except DriverError:
87
+ logger.debug(
88
+ "Skipping non-public template input %s",
89
+ key,
90
+ exc_info=True,
91
+ )
92
+ context[key] = ""
93
+ buffer = io.BytesIO()
94
+ tpl.render(context)
95
+ tpl.save(buffer)
96
+ buffer.seek(0)
97
+ document = Document(buffer)
98
+ return DocxSession(
99
+ document_id=str(metadata.get("documentId", "")),
100
+ driver_id=self.driver_id,
101
+ document=document,
102
+ context=context,
103
+ )
104
+
105
+ def apply_operation(
106
+ self, session: DocxSession, op: DocumentOperation
107
+ ) -> None:
108
+ """Apply flow / named-value operations."""
109
+ assert_op_allowed(self.manifest, op)
110
+ data = op.model_dump(mode="python", by_alias=True)
111
+ session.ops_applied.append(op.model_dump(mode="json", by_alias=True))
112
+ if op.op == "flow.insert_paragraph":
113
+ text = "".join(run["text"] for run in data["runs"])
114
+ session.document.add_paragraph(text)
115
+ elif op.op == "flow.insert_heading":
116
+ session.document.add_heading(
117
+ str(data["text"]), level=int(data["level"])
118
+ )
119
+ elif op.op == "replace_named_value":
120
+ value = TypeAdapter(WorkspaceValue).validate_python(data["value"])
121
+ rendered = str(public_literal_value(value))
122
+ needle = "{{" + str(data["name"]) + "}}"
123
+ for paragraph in session.document.paragraphs:
124
+ if needle in paragraph.text:
125
+ paragraph.text = paragraph.text.replace(needle, rendered)
126
+
127
+ def finalize(self, session: DocxSession) -> None:
128
+ """No-op finalize."""
129
+ return
130
+
131
+ def serialize(self, session: DocxSession) -> bytes:
132
+ """Return .docx bytes."""
133
+ buffer = io.BytesIO()
134
+ session.document.save(buffer)
135
+ return buffer.getvalue()
136
+
137
+ def preview(self, session: DocxSession) -> DocumentPreview:
138
+ """Approximate HTML from paragraphs."""
139
+ parts = ['<div class="roborean-docx">']
140
+ for paragraph in session.document.paragraphs:
141
+ style = paragraph.style.name if paragraph.style else ""
142
+ tag = "p"
143
+ if style.startswith("Heading"):
144
+ try:
145
+ level = int(style.replace("Heading ", ""))
146
+ tag = f"h{level}"
147
+ except ValueError:
148
+ tag = "h2"
149
+ parts.append(f"<{tag}>{paragraph.text}</{tag}>")
150
+ parts.append("</div>")
151
+ return DocumentPreview(
152
+ documentId=session.document_id,
153
+ mode="html",
154
+ body="".join(parts),
155
+ warnings=[],
156
+ generatedAt=datetime.now(UTC).isoformat(),
157
+ renderer={
158
+ "package": "roborean-documents-docx",
159
+ "version": "0.3.0",
160
+ },
161
+ )
162
+
163
+
164
+ def create_driver() -> DocxDocumentDriver:
165
+ """Entry-point factory."""
166
+ return DocxDocumentDriver()
167
+
168
+
169
+ def docx_paragraph_texts(data: bytes) -> list[str]:
170
+ """Extract paragraph texts for semantic compare."""
171
+ document = Document(io.BytesIO(data))
172
+ return [paragraph.text for paragraph in document.paragraphs]
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: roborean-documents-docx
3
+ Version: 0.1.2
4
+ Summary: DOCX document driver for Roborean
5
+ Project-URL: Homepage, https://github.com/TNick/roborean
6
+ Project-URL: Repository, https://github.com/TNick/roborean
7
+ License-Expression: MIT
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: docxtpl>=0.16
10
+ Requires-Dist: python-docx>=1.1
11
+ Requires-Dist: roborean-documents-base>=0.1.1
12
+ Requires-Dist: roborean-spec>=0.1.1
@@ -0,0 +1,5 @@
1
+ roborean_documents_docx/driver.py,sha256=j80PDNqeGcinE_xEHAWEuAvOYD0SIKDo6y4XAygxSS0,5911
2
+ roborean_documents_docx-0.1.2.dist-info/METADATA,sha256=Guw3TKuQ56g-GMZI1p-vQVhbfXaKciLPQ6DIAlBmKK4,417
3
+ roborean_documents_docx-0.1.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
4
+ roborean_documents_docx-0.1.2.dist-info/entry_points.txt,sha256=RotwSoLCOJmELnOGdHIAC4V8hsYIgZg88PSoT0aqC2U,89
5
+ roborean_documents_docx-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [roborean.document_drivers]
2
+ roborean.docx = roborean_documents_docx.driver:create_driver