roborean-documents-xlsx 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,14 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - `roborean.xlsx` driver with sheet operations and HTML preview.
8
+
9
+ ### Changed
10
+
11
+ ### Fixed
12
+
13
+ - Lower inter-package dependency pins to `>=0.1.1` to match lockstep
14
+ release versions.
@@ -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,27 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "roborean-documents-xlsx"
7
+ version = "0.1.2"
8
+ description = "XLSX 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
+ "openpyxl>=3.1",
15
+ "defusedxml>=0.7",
16
+ ]
17
+
18
+ [project.entry-points."roborean.document_drivers"]
19
+ "roborean.xlsx" = "roborean_documents_xlsx.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_xlsx"]
@@ -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