roborean-documents-xlsx 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,161 @@
|
|
|
1
|
+
"""XLSX document driver using openpyxl."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
from openpyxl import Workbook, load_workbook
|
|
9
|
+
from pydantic import TypeAdapter
|
|
10
|
+
from roborean_documents_base.capabilities import assert_op_allowed
|
|
11
|
+
from roborean_documents_base.resolve_values import public_literal_value
|
|
12
|
+
from roborean_spec import (
|
|
13
|
+
DocumentDriverManifest,
|
|
14
|
+
DocumentOperation,
|
|
15
|
+
DocumentPreview,
|
|
16
|
+
TemplateManifest,
|
|
17
|
+
WorkspaceValue,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class XlsxSession:
|
|
23
|
+
"""Open workbook session."""
|
|
24
|
+
|
|
25
|
+
document_id: str
|
|
26
|
+
driver_id: str
|
|
27
|
+
workbook: Any
|
|
28
|
+
ops_applied: list[dict[str, Any]] = field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class XlsxDocumentDriver:
|
|
32
|
+
"""Spreadsheet driver with sheet.* operations."""
|
|
33
|
+
|
|
34
|
+
driver_id = "roborean.xlsx"
|
|
35
|
+
manifest = DocumentDriverManifest(
|
|
36
|
+
driverId="roborean.xlsx",
|
|
37
|
+
version="0.3.0",
|
|
38
|
+
irFamily="sheet",
|
|
39
|
+
capabilities=[
|
|
40
|
+
"set_metadata",
|
|
41
|
+
"replace_named_value",
|
|
42
|
+
"sheet.set_cell",
|
|
43
|
+
"sheet.set_formula",
|
|
44
|
+
"sheet.ensure_sheet",
|
|
45
|
+
"finalize",
|
|
46
|
+
],
|
|
47
|
+
supportsPreview=True,
|
|
48
|
+
supportsBrowserExecution=False,
|
|
49
|
+
supportsDiff=True,
|
|
50
|
+
requiresBackend=True,
|
|
51
|
+
templateMediaTypes=[
|
|
52
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
53
|
+
],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
"""Initialize empty template state."""
|
|
58
|
+
self._template: bytes | None = None
|
|
59
|
+
self._manifest: TemplateManifest | None = None
|
|
60
|
+
|
|
61
|
+
def load_template(self, template_ref, *, store, manifest) -> None:
|
|
62
|
+
"""Load .xlsx template bytes."""
|
|
63
|
+
self._template = store.load_bytes(template_ref)
|
|
64
|
+
self._manifest = manifest
|
|
65
|
+
|
|
66
|
+
def begin_session(
|
|
67
|
+
self, workspace, metadata: Mapping[str, Any]
|
|
68
|
+
) -> XlsxSession:
|
|
69
|
+
"""Open a workbook from the template or a blank book."""
|
|
70
|
+
if self._template:
|
|
71
|
+
workbook = load_workbook(io.BytesIO(self._template))
|
|
72
|
+
else:
|
|
73
|
+
workbook = Workbook()
|
|
74
|
+
return XlsxSession(
|
|
75
|
+
document_id=str(metadata.get("documentId", "")),
|
|
76
|
+
driver_id=self.driver_id,
|
|
77
|
+
workbook=workbook,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def apply_operation(
|
|
81
|
+
self, session: XlsxSession, op: DocumentOperation
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Apply one sheet operation."""
|
|
84
|
+
assert_op_allowed(self.manifest, op)
|
|
85
|
+
data = op.model_dump(mode="python", by_alias=True)
|
|
86
|
+
session.ops_applied.append(op.model_dump(mode="json", by_alias=True))
|
|
87
|
+
if op.op == "sheet.ensure_sheet":
|
|
88
|
+
name = str(data["name"])
|
|
89
|
+
if name not in session.workbook.sheetnames:
|
|
90
|
+
session.workbook.create_sheet(name)
|
|
91
|
+
return
|
|
92
|
+
if op.op == "sheet.set_cell":
|
|
93
|
+
sheet = session.workbook[str(data["sheet"])]
|
|
94
|
+
value = TypeAdapter(WorkspaceValue).validate_python(data["value"])
|
|
95
|
+
sheet[str(data["cell"])] = public_literal_value(value)
|
|
96
|
+
return
|
|
97
|
+
if op.op == "sheet.set_formula":
|
|
98
|
+
sheet = session.workbook[str(data["sheet"])]
|
|
99
|
+
formula = str(data["formula"])
|
|
100
|
+
if not formula.startswith("="):
|
|
101
|
+
formula = "=" + formula
|
|
102
|
+
sheet[str(data["cell"])] = formula
|
|
103
|
+
|
|
104
|
+
def finalize(self, session: XlsxSession) -> None:
|
|
105
|
+
"""No-op finalize."""
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
def serialize(self, session: XlsxSession) -> bytes:
|
|
109
|
+
"""Return .xlsx bytes."""
|
|
110
|
+
buffer = io.BytesIO()
|
|
111
|
+
session.workbook.save(buffer)
|
|
112
|
+
return buffer.getvalue()
|
|
113
|
+
|
|
114
|
+
def preview(self, session: XlsxSession) -> DocumentPreview:
|
|
115
|
+
"""Build a simplified HTML table preview."""
|
|
116
|
+
max_rows = 50
|
|
117
|
+
parts = ['<div class="roborean-xlsx">']
|
|
118
|
+
for name in session.workbook.sheetnames:
|
|
119
|
+
sheet = session.workbook[name]
|
|
120
|
+
parts.append(f"<h3>{name}</h3><table>")
|
|
121
|
+
for index, row in enumerate(sheet.iter_rows(values_only=True)):
|
|
122
|
+
if index >= max_rows:
|
|
123
|
+
break
|
|
124
|
+
cells = "".join(
|
|
125
|
+
f"<td>{'' if c is None else c}</td>" for c in row
|
|
126
|
+
)
|
|
127
|
+
parts.append(f"<tr>{cells}</tr>")
|
|
128
|
+
parts.append("</table>")
|
|
129
|
+
parts.append("</div>")
|
|
130
|
+
return DocumentPreview(
|
|
131
|
+
documentId=session.document_id,
|
|
132
|
+
mode="html",
|
|
133
|
+
body="".join(parts),
|
|
134
|
+
warnings=[],
|
|
135
|
+
generatedAt=datetime.now(UTC).isoformat(),
|
|
136
|
+
renderer={
|
|
137
|
+
"package": "roborean-documents-xlsx",
|
|
138
|
+
"version": "0.3.0",
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def create_driver() -> XlsxDocumentDriver:
|
|
144
|
+
"""Entry-point factory."""
|
|
145
|
+
return XlsxDocumentDriver()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def xlsx_semantic_equal(a: bytes, b: bytes) -> bool:
|
|
149
|
+
"""Compare sheet values/formulas; ignore workbook metadata."""
|
|
150
|
+
left = load_workbook(io.BytesIO(a), data_only=False)
|
|
151
|
+
right = load_workbook(io.BytesIO(b), data_only=False)
|
|
152
|
+
if left.sheetnames != right.sheetnames:
|
|
153
|
+
return False
|
|
154
|
+
for name in left.sheetnames:
|
|
155
|
+
lsheet = left[name]
|
|
156
|
+
rsheet = right[name]
|
|
157
|
+
lvals = [[cell.value for cell in row] for row in lsheet.iter_rows()]
|
|
158
|
+
rvals = [[cell.value for cell in row] for row in rsheet.iter_rows()]
|
|
159
|
+
if lvals != rvals:
|
|
160
|
+
return False
|
|
161
|
+
return True
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: roborean-documents-xlsx
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: XLSX 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: defusedxml>=0.7
|
|
10
|
+
Requires-Dist: openpyxl>=3.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_xlsx/driver.py,sha256=-J8xmKQXJIMwj828LiojHlFBR1nVHfhIKHfchJ80EHQ,5417
|
|
2
|
+
roborean_documents_xlsx-0.1.2.dist-info/METADATA,sha256=GvWLMFP5xu8R6ytjzxdPUHQHgKctFway7b2K3zJJP7w,416
|
|
3
|
+
roborean_documents_xlsx-0.1.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
4
|
+
roborean_documents_xlsx-0.1.2.dist-info/entry_points.txt,sha256=8JCbOouvoyHH4YNlPgBhpiPwyLan3o0c4aF1X8zIZ2A,89
|
|
5
|
+
roborean_documents_xlsx-0.1.2.dist-info/RECORD,,
|