roborean-documents-docx 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.
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
venv/
|
|
2
|
+
venv-qt5/
|
|
3
|
+
venv-qt6/
|
|
4
|
+
.venv/
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.mypy_cache/
|
|
9
|
+
.coverage
|
|
10
|
+
htmlcov/
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
*.tsbuildinfo
|
|
15
|
+
node_modules/
|
|
16
|
+
.pnpm-store/
|
|
17
|
+
.DS_Store
|
|
18
|
+
.idea/
|
|
19
|
+
.vscode/*.local
|
|
20
|
+
playground/
|
|
21
|
+
.e2e-ai/
|
|
22
|
+
**/test-results/
|
|
23
|
+
**/playwright-report/
|
|
24
|
+
research/
|
|
25
|
+
.roborean/
|
|
26
|
+
.roborean-sql-artifacts/
|
|
27
|
+
*.db
|
|
@@ -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,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "roborean-documents-docx"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "DOCX document driver for Roborean"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"roborean-documents-base>=0.1.1",
|
|
13
|
+
"roborean-spec>=0.1.1",
|
|
14
|
+
"python-docx>=1.1",
|
|
15
|
+
"docxtpl>=0.16",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.entry-points."roborean.document_drivers"]
|
|
19
|
+
"roborean.docx" = "roborean_documents_docx.driver:create_driver"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/TNick/roborean"
|
|
24
|
+
Repository = "https://github.com/TNick/roborean"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/roborean_documents_docx"]
|
|
@@ -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]
|