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,,
@@ -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.xlsx = roborean_documents_xlsx.driver:create_driver