xlsxlite 1.1.0__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.
- xlsxlite/__init__.py +0 -0
- xlsxlite/test/__init__.py +0 -0
- xlsxlite/test/base.py +58 -0
- xlsxlite/test/test_perf.py +76 -0
- xlsxlite/test/test_utils.py +15 -0
- xlsxlite/test/test_writer.py +88 -0
- xlsxlite/utils.py +14 -0
- xlsxlite/writer.py +278 -0
- xlsxlite-1.1.0.dist-info/METADATA +64 -0
- xlsxlite-1.1.0.dist-info/RECORD +12 -0
- xlsxlite-1.1.0.dist-info/WHEEL +4 -0
- xlsxlite-1.1.0.dist-info/licenses/LICENSE +21 -0
xlsxlite/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
xlsxlite/test/base.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
import shutil
|
|
4
|
+
import unittest
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def tests_dir():
|
|
10
|
+
os.mkdir("_tests")
|
|
11
|
+
yield
|
|
12
|
+
shutil.rmtree("_tests")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class XLSXTest(unittest.TestCase):
|
|
16
|
+
def assertExcelRow(self, sheet, row_num, values, tz=None):
|
|
17
|
+
"""
|
|
18
|
+
Asserts the cell values in the given worksheet row. Date values are converted using the provided timezone.
|
|
19
|
+
"""
|
|
20
|
+
expected_values = []
|
|
21
|
+
for expected in values:
|
|
22
|
+
# if expected value is datetime, localize and remove microseconds
|
|
23
|
+
if isinstance(expected, datetime):
|
|
24
|
+
expected = expected.astimezone(tz).replace(microsecond=0, tzinfo=None)
|
|
25
|
+
|
|
26
|
+
expected_values.append(expected)
|
|
27
|
+
|
|
28
|
+
rows = tuple(sheet.rows)
|
|
29
|
+
|
|
30
|
+
actual_values = []
|
|
31
|
+
for cell in rows[row_num]:
|
|
32
|
+
actual = cell.value
|
|
33
|
+
|
|
34
|
+
if actual is None:
|
|
35
|
+
actual = ""
|
|
36
|
+
|
|
37
|
+
if isinstance(actual, datetime):
|
|
38
|
+
actual = actual
|
|
39
|
+
|
|
40
|
+
actual_values.append(actual)
|
|
41
|
+
|
|
42
|
+
for index, expected in enumerate(expected_values):
|
|
43
|
+
actual = actual_values[index]
|
|
44
|
+
|
|
45
|
+
if isinstance(expected, datetime):
|
|
46
|
+
close_enough = abs(expected - actual) < timedelta(seconds=1)
|
|
47
|
+
assert close_enough, f"Datetime value {expected} doesn't match {actual}"
|
|
48
|
+
else:
|
|
49
|
+
assert expected == actual
|
|
50
|
+
|
|
51
|
+
def assertExcelSheet(self, sheet, rows, tz=None):
|
|
52
|
+
"""
|
|
53
|
+
Asserts the row values in the given worksheet
|
|
54
|
+
"""
|
|
55
|
+
assert len(list(sheet.rows)) == len(rows)
|
|
56
|
+
|
|
57
|
+
for r, row in enumerate(rows):
|
|
58
|
+
self.assertExcelRow(sheet, r, row, tz)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import random
|
|
3
|
+
import string
|
|
4
|
+
import xlsxwriter
|
|
5
|
+
|
|
6
|
+
from openpyxl import Workbook
|
|
7
|
+
from openpyxl.cell.cell import WriteOnlyCell
|
|
8
|
+
from openpyxl.cell._writer import etree_write_cell
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
from xlsxlite.writer import XLSXBook
|
|
11
|
+
from .base import tests_dir # noqa
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
NUM_ROWS = 1000
|
|
15
|
+
NUM_COLS = 10
|
|
16
|
+
|
|
17
|
+
# generate some random strings to use as cell values
|
|
18
|
+
DATA = ["".join(random.choices(string.ascii_uppercase + string.digits, k=16)) for d in range(1000)]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.usefixtures("tests_dir")
|
|
22
|
+
def test_xlxslite():
|
|
23
|
+
book = XLSXBook()
|
|
24
|
+
sheet1 = book.add_sheet("Sheet1")
|
|
25
|
+
|
|
26
|
+
for r in range(NUM_ROWS):
|
|
27
|
+
row = [DATA[(r * c) % len(DATA)] for c in range(NUM_COLS)]
|
|
28
|
+
|
|
29
|
+
sheet1.append_row(*row)
|
|
30
|
+
|
|
31
|
+
book.finalize(to_file="_tests/test.xlsx")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.usefixtures("tests_dir")
|
|
35
|
+
@patch("openpyxl.cell._writer.write_cell")
|
|
36
|
+
def test_openpyxl_etree(mock_write_cell):
|
|
37
|
+
mock_write_cell.side_effect = etree_write_cell
|
|
38
|
+
|
|
39
|
+
book = Workbook(write_only=True)
|
|
40
|
+
sheet1 = book.create_sheet("Sheet1")
|
|
41
|
+
|
|
42
|
+
for r in range(NUM_ROWS):
|
|
43
|
+
row = [DATA[(r * c) % len(DATA)] for c in range(NUM_COLS)]
|
|
44
|
+
|
|
45
|
+
cells = [WriteOnlyCell(sheet1, value=v) for v in row]
|
|
46
|
+
sheet1.append(cells)
|
|
47
|
+
|
|
48
|
+
book.save("_tests/test.xlsx")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.usefixtures("tests_dir")
|
|
52
|
+
def test_openpyxl_lxml():
|
|
53
|
+
book = Workbook(write_only=True)
|
|
54
|
+
sheet1 = book.create_sheet("Sheet1")
|
|
55
|
+
|
|
56
|
+
for r in range(NUM_ROWS):
|
|
57
|
+
row = [DATA[(r * c) % len(DATA)] for c in range(NUM_COLS)]
|
|
58
|
+
|
|
59
|
+
cells = [WriteOnlyCell(sheet1, value=v) for v in row]
|
|
60
|
+
sheet1.append(cells)
|
|
61
|
+
|
|
62
|
+
book.save("_tests/test.xlsx")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.usefixtures("tests_dir")
|
|
66
|
+
def test_xlsxwriter():
|
|
67
|
+
book = xlsxwriter.Workbook("_tests/test.xlsx")
|
|
68
|
+
sheet1 = book.add_worksheet()
|
|
69
|
+
|
|
70
|
+
for r in range(NUM_ROWS):
|
|
71
|
+
row = [DATA[(r * c) % len(DATA)] for c in range(NUM_COLS)]
|
|
72
|
+
|
|
73
|
+
for c, val in enumerate(row):
|
|
74
|
+
sheet1.write(r, c, val)
|
|
75
|
+
|
|
76
|
+
book.close()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import pytz
|
|
5
|
+
|
|
6
|
+
from xlsxlite.utils import datetime_to_serial
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_datetime_to_serial():
|
|
10
|
+
assert datetime_to_serial(datetime(2013, 1, 1, 12, 0, 0)) == 41275.5
|
|
11
|
+
assert datetime_to_serial(datetime(2018, 6, 15, 11, 24, 30, 0)) == 43266.47534722222
|
|
12
|
+
|
|
13
|
+
# try with a non-naive datetime
|
|
14
|
+
with pytest.raises(ValueError):
|
|
15
|
+
datetime_to_serial(datetime(2018, 6, 15, 11, 24, 30, 0, pytz.UTC))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from openpyxl.reader.excel import load_workbook
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
from xlsxlite.writer import XLSXBook
|
|
7
|
+
|
|
8
|
+
from .base import XLSXTest, tests_dir # noqa
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.usefixtures("tests_dir")
|
|
12
|
+
class BookTest(XLSXTest):
|
|
13
|
+
def test_empty(self):
|
|
14
|
+
book = XLSXBook()
|
|
15
|
+
book.finalize(to_file="_tests/empty.xlsx")
|
|
16
|
+
|
|
17
|
+
book = load_workbook(filename="_tests/empty.xlsx")
|
|
18
|
+
assert len(book.worksheets) == 1
|
|
19
|
+
assert book.worksheets[0].title == "Sheet1"
|
|
20
|
+
|
|
21
|
+
def test_simple(self):
|
|
22
|
+
book = XLSXBook()
|
|
23
|
+
sheet1 = book.add_sheet("People")
|
|
24
|
+
sheet1.append_row("Name", "Email")
|
|
25
|
+
sheet1.append_row("Jim", "jim@acme.com")
|
|
26
|
+
sheet1.append_row("Bob", "bob@acme.com")
|
|
27
|
+
|
|
28
|
+
book.add_sheet("Empty")
|
|
29
|
+
|
|
30
|
+
# insert a new sheet at a specific index
|
|
31
|
+
book.add_sheet("New first", index=0)
|
|
32
|
+
|
|
33
|
+
book.finalize(to_file="_tests/simple.xlsx")
|
|
34
|
+
|
|
35
|
+
book = load_workbook(filename="_tests/simple.xlsx")
|
|
36
|
+
assert len(book.worksheets) == 3
|
|
37
|
+
|
|
38
|
+
sheet1, sheet2, sheet3 = book.worksheets
|
|
39
|
+
assert sheet1.title == "New first"
|
|
40
|
+
assert sheet2.title == "People"
|
|
41
|
+
assert sheet3.title == "Empty"
|
|
42
|
+
|
|
43
|
+
self.assertExcelSheet(sheet1, [])
|
|
44
|
+
self.assertExcelSheet(sheet2, [("Name", "Email"), ("Jim", "jim@acme.com"), ("Bob", "bob@acme.com")])
|
|
45
|
+
self.assertExcelSheet(sheet3, [])
|
|
46
|
+
|
|
47
|
+
def test_cell_types(self):
|
|
48
|
+
d1 = datetime(2013, 1, 1, 12, 0, 0)
|
|
49
|
+
|
|
50
|
+
book = XLSXBook()
|
|
51
|
+
sheet1 = book.add_sheet("Test")
|
|
52
|
+
sheet1.append_row("str", True, False, 3, 1.23, d1)
|
|
53
|
+
|
|
54
|
+
# try to write a cell value with an unsupported type
|
|
55
|
+
with pytest.raises(ValueError):
|
|
56
|
+
sheet1.append_row(timedelta(days=1))
|
|
57
|
+
|
|
58
|
+
book.finalize(to_file="_tests/types.xlsx")
|
|
59
|
+
|
|
60
|
+
book = load_workbook(filename="_tests/types.xlsx")
|
|
61
|
+
self.assertExcelSheet(book.worksheets[0], [("str", True, False, 3, 1.23, d1)])
|
|
62
|
+
|
|
63
|
+
def test_escaping(self):
|
|
64
|
+
book = XLSXBook()
|
|
65
|
+
sheet1 = book.add_sheet("Test")
|
|
66
|
+
sheet1.append_row('< & > " ! =')
|
|
67
|
+
book.finalize(to_file="_tests/escaped.xlsx")
|
|
68
|
+
|
|
69
|
+
book = load_workbook(filename="_tests/escaped.xlsx")
|
|
70
|
+
self.assertExcelSheet(book.worksheets[0], [('< & > " ! =',)])
|
|
71
|
+
|
|
72
|
+
def test_sheet_limits(self):
|
|
73
|
+
book = XLSXBook()
|
|
74
|
+
sheet1 = book.add_sheet("Sheet1")
|
|
75
|
+
|
|
76
|
+
# try to add row with too many columns
|
|
77
|
+
column = ["x"] * 20000
|
|
78
|
+
with pytest.raises(ValueError):
|
|
79
|
+
sheet1.append_row(*column)
|
|
80
|
+
|
|
81
|
+
# try to add more rows than allowed
|
|
82
|
+
with patch("xlsxlite.writer.XLSXSheet.MAX_ROWS", 3):
|
|
83
|
+
sheet1.append_row("x")
|
|
84
|
+
sheet1.append_row("x")
|
|
85
|
+
sheet1.append_row("x")
|
|
86
|
+
|
|
87
|
+
with pytest.raises(ValueError):
|
|
88
|
+
sheet1.append_row("x")
|
xlsxlite/utils.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def datetime_to_serial(dt):
|
|
5
|
+
"""
|
|
6
|
+
Converts the given datetime to the Excel serial format
|
|
7
|
+
"""
|
|
8
|
+
if dt.tzinfo:
|
|
9
|
+
raise ValueError("Doesn't support datetimes with timezones")
|
|
10
|
+
|
|
11
|
+
temp = datetime(1899, 12, 30)
|
|
12
|
+
delta = dt - temp
|
|
13
|
+
|
|
14
|
+
return delta.days + (float(delta.seconds) + float(delta.microseconds) / 1E6) / (60 * 60 * 24)
|
xlsxlite/writer.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import tempfile
|
|
4
|
+
import xml.etree.ElementTree as ET
|
|
5
|
+
import zipfile
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from xml.sax.saxutils import escape
|
|
8
|
+
|
|
9
|
+
from .utils import datetime_to_serial
|
|
10
|
+
|
|
11
|
+
XML_HEADER = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n"""
|
|
12
|
+
|
|
13
|
+
WORKBOOK_HEADER = """<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"""
|
|
14
|
+
WORKSHEET_HEADER = """<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"""
|
|
15
|
+
|
|
16
|
+
MINIMAL_STYLESHEET = """<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
17
|
+
<numFmts count="1">
|
|
18
|
+
<numFmt formatCode="yyyy-mm-dd h:mm:ss" numFmtId="164"/>
|
|
19
|
+
</numFmts>
|
|
20
|
+
<fonts count="1">
|
|
21
|
+
<font>
|
|
22
|
+
<name val="Calibri"/>
|
|
23
|
+
<family val="2"/>
|
|
24
|
+
<color theme="1"/>
|
|
25
|
+
<sz val="11"/>
|
|
26
|
+
<scheme val="minor"/>
|
|
27
|
+
</font>
|
|
28
|
+
</fonts>
|
|
29
|
+
<fills count="2">
|
|
30
|
+
<fill><patternFill/></fill>
|
|
31
|
+
<fill><patternFill patternType="gray125"/></fill>
|
|
32
|
+
</fills>
|
|
33
|
+
<borders count="1">
|
|
34
|
+
<border>
|
|
35
|
+
<left/>
|
|
36
|
+
<right/>
|
|
37
|
+
<top/>
|
|
38
|
+
<bottom/>
|
|
39
|
+
<diagonal/>
|
|
40
|
+
</border>
|
|
41
|
+
</borders>
|
|
42
|
+
<cellStyleXfs count="1">
|
|
43
|
+
<xf borderId="0" fillId="0" fontId="0" numFmtId="0"/>
|
|
44
|
+
</cellStyleXfs>
|
|
45
|
+
<cellXfs count="2">
|
|
46
|
+
<xf borderId="0" fillId="0" fontId="0" numFmtId="0" pivotButton="0" quotePrefix="0" xfId="0"/>
|
|
47
|
+
<xf borderId="0" fillId="0" fontId="0" numFmtId="164" pivotButton="0" quotePrefix="0" xfId="0"/>
|
|
48
|
+
</cellXfs>
|
|
49
|
+
<cellStyles count="1">
|
|
50
|
+
<cellStyle builtinId="0" hidden="0" name="Normal" xfId="0"/>
|
|
51
|
+
</cellStyles>
|
|
52
|
+
</styleSheet>"""
|
|
53
|
+
|
|
54
|
+
# use a nice big 1MB I/O buffer for the worksheet files
|
|
55
|
+
WORKSHEET_IO_BUFFER = 1048576
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class XLSXSheet:
|
|
59
|
+
"""
|
|
60
|
+
A worksheet within a XLSX workbook
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
MAX_ROWS = 1048576
|
|
64
|
+
MAX_COLS = 16384
|
|
65
|
+
|
|
66
|
+
def __init__(self, _id, name, path):
|
|
67
|
+
self.id = _id
|
|
68
|
+
self.name = name
|
|
69
|
+
self.path = path
|
|
70
|
+
self.relationshipId = f"rId{_id}"
|
|
71
|
+
|
|
72
|
+
self.num_rows = 0
|
|
73
|
+
|
|
74
|
+
self.file = open(path, "w", encoding="utf-8", buffering=WORKSHEET_IO_BUFFER)
|
|
75
|
+
self.file.write(XML_HEADER)
|
|
76
|
+
self.file.write(WORKSHEET_HEADER)
|
|
77
|
+
self.file.write("<sheetData>")
|
|
78
|
+
|
|
79
|
+
def append_row(self, *columns):
|
|
80
|
+
"""
|
|
81
|
+
Appends a new row to this sheet
|
|
82
|
+
"""
|
|
83
|
+
if len(columns) > self.MAX_COLS:
|
|
84
|
+
raise ValueError(f"rows can have a maximum of {self.MAX_COLS} columns")
|
|
85
|
+
|
|
86
|
+
if self.num_rows >= self.MAX_ROWS:
|
|
87
|
+
raise ValueError(f"sheet already has the maximum of {self.MAX_ROWS} rows")
|
|
88
|
+
|
|
89
|
+
row = "<row>"
|
|
90
|
+
for val in columns:
|
|
91
|
+
if isinstance(val, str):
|
|
92
|
+
row += f'<c t="inlineStr"><is><t>{escape(val)}</t></is></c>'
|
|
93
|
+
elif isinstance(val, bool):
|
|
94
|
+
row += f'<c t="b"><v>{int(val)}</v></c>'
|
|
95
|
+
elif isinstance(val, (int, float)):
|
|
96
|
+
row += f'<c t="n"><v>{str(val)}</v></c>'
|
|
97
|
+
elif isinstance(val, datetime):
|
|
98
|
+
row += f'<c t="n" s="1"><v>{datetime_to_serial(val)}</v></c>'
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(f"Unsupported type in column data: {type(val)}")
|
|
101
|
+
|
|
102
|
+
row += "</row>"
|
|
103
|
+
|
|
104
|
+
self.file.write(row)
|
|
105
|
+
self.num_rows += 1
|
|
106
|
+
|
|
107
|
+
def finalize(self):
|
|
108
|
+
"""
|
|
109
|
+
Finalizes this sheet so that its XML file is valid
|
|
110
|
+
"""
|
|
111
|
+
self.file.write("</sheetData></worksheet>")
|
|
112
|
+
self.file.close()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class XLSXBook:
|
|
116
|
+
"""
|
|
117
|
+
An XLSX workbook
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self):
|
|
121
|
+
self.base_dir = tempfile.mkdtemp()
|
|
122
|
+
self.app_dir = os.path.join(self.base_dir, "xl")
|
|
123
|
+
self.sheets = []
|
|
124
|
+
|
|
125
|
+
os.mkdir(self.app_dir)
|
|
126
|
+
os.mkdir(os.path.join(self.app_dir, "worksheets"))
|
|
127
|
+
|
|
128
|
+
def add_sheet(self, name, index=-1):
|
|
129
|
+
"""
|
|
130
|
+
Adds a new worksheet to this workbook with the given name
|
|
131
|
+
"""
|
|
132
|
+
_id = str(len(self.sheets) + 1)
|
|
133
|
+
path = os.path.join(self.app_dir, f"worksheets/sheet{_id}.xml")
|
|
134
|
+
sheet = XLSXSheet(_id, name, path)
|
|
135
|
+
|
|
136
|
+
if index < 0:
|
|
137
|
+
index = len(self.sheets)
|
|
138
|
+
|
|
139
|
+
self.sheets.insert(index, sheet)
|
|
140
|
+
return sheet
|
|
141
|
+
|
|
142
|
+
def _create_content_types(self):
|
|
143
|
+
types = ET.Element("Types", {"xmlns": "http://schemas.openxmlformats.org/package/2006/content-types"})
|
|
144
|
+
ET.SubElement(
|
|
145
|
+
types,
|
|
146
|
+
"Default",
|
|
147
|
+
{"Extension": "rels", "ContentType": "application/vnd.openxmlformats-package.relationships+xml"},
|
|
148
|
+
)
|
|
149
|
+
ET.SubElement(types, "Default", {"Extension": "xml", "ContentType": "application/xml"})
|
|
150
|
+
ET.SubElement(
|
|
151
|
+
types,
|
|
152
|
+
"Override",
|
|
153
|
+
{
|
|
154
|
+
"PartName": "/xl/styles.xml",
|
|
155
|
+
"ContentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
ET.SubElement(
|
|
159
|
+
types,
|
|
160
|
+
"Override",
|
|
161
|
+
{
|
|
162
|
+
"PartName": "/xl/workbook.xml",
|
|
163
|
+
"ContentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
for sheet in self.sheets:
|
|
168
|
+
rel_path = sheet.path[len(self.base_dir) :]
|
|
169
|
+
ET.SubElement(
|
|
170
|
+
types,
|
|
171
|
+
"Override",
|
|
172
|
+
{
|
|
173
|
+
"PartName": rel_path,
|
|
174
|
+
"ContentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
with open(os.path.join(self.base_dir, "[Content_Types].xml"), "w", encoding="utf-8") as f:
|
|
179
|
+
f.write(XML_HEADER)
|
|
180
|
+
f.write(ET.tostring(types, encoding="unicode"))
|
|
181
|
+
|
|
182
|
+
def _create_root_rels(self):
|
|
183
|
+
os.mkdir(os.path.join(self.base_dir, "_rels"))
|
|
184
|
+
|
|
185
|
+
relationships = ET.Element(
|
|
186
|
+
"Relationships", {"xmlns": "http://schemas.openxmlformats.org/package/2006/relationships"}
|
|
187
|
+
)
|
|
188
|
+
ET.SubElement(
|
|
189
|
+
relationships,
|
|
190
|
+
"Relationship",
|
|
191
|
+
{
|
|
192
|
+
"Id": "rId1",
|
|
193
|
+
"Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
194
|
+
"Target": "xl/workbook.xml",
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
with open(os.path.join(self.base_dir, "_rels/.rels"), "w", encoding="utf-8") as f:
|
|
199
|
+
f.write(XML_HEADER)
|
|
200
|
+
f.write(ET.tostring(relationships, encoding="unicode"))
|
|
201
|
+
|
|
202
|
+
def _create_app_rels(self):
|
|
203
|
+
os.mkdir(os.path.join(self.app_dir, "_rels"))
|
|
204
|
+
|
|
205
|
+
relationships = ET.Element(
|
|
206
|
+
"Relationships", {"xmlns": "http://schemas.openxmlformats.org/package/2006/relationships"}
|
|
207
|
+
)
|
|
208
|
+
for sheet in self.sheets:
|
|
209
|
+
rel_path = os.path.relpath(sheet.path, start=self.app_dir)
|
|
210
|
+
|
|
211
|
+
ET.SubElement(
|
|
212
|
+
relationships,
|
|
213
|
+
"Relationship",
|
|
214
|
+
{
|
|
215
|
+
"Id": sheet.relationshipId,
|
|
216
|
+
"Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
|
|
217
|
+
"Target": rel_path,
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
ET.SubElement(
|
|
222
|
+
relationships,
|
|
223
|
+
"Relationship",
|
|
224
|
+
{
|
|
225
|
+
"Id": "rIdStyles",
|
|
226
|
+
"Type": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
227
|
+
"Target": "styles.xml",
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
with open(os.path.join(self.app_dir, "_rels/workbook.xml.rels"), "w", encoding="utf-8") as f:
|
|
232
|
+
f.write(XML_HEADER)
|
|
233
|
+
f.write(ET.tostring(relationships, encoding="unicode"))
|
|
234
|
+
|
|
235
|
+
def _create_styles(self):
|
|
236
|
+
with open(os.path.join(self.app_dir, "styles.xml"), "w", encoding="utf-8") as f:
|
|
237
|
+
f.write(XML_HEADER)
|
|
238
|
+
f.write(MINIMAL_STYLESHEET)
|
|
239
|
+
|
|
240
|
+
def _create_workbook(self):
|
|
241
|
+
sheets = ET.Element("sheets")
|
|
242
|
+
for sheet in self.sheets:
|
|
243
|
+
ET.SubElement(sheets, "sheet", {"name": sheet.name, "sheetId": sheet.id, "r:id": sheet.relationshipId})
|
|
244
|
+
|
|
245
|
+
with open(os.path.join(self.base_dir, "xl/workbook.xml"), "w", encoding="utf-8") as f:
|
|
246
|
+
f.write(XML_HEADER)
|
|
247
|
+
f.write(WORKBOOK_HEADER)
|
|
248
|
+
f.write(ET.tostring(sheets, encoding="unicode"))
|
|
249
|
+
f.write("</workbook>")
|
|
250
|
+
|
|
251
|
+
def _archive_dir(self, to_file):
|
|
252
|
+
archive = zipfile.ZipFile(to_file, "w", zipfile.ZIP_DEFLATED)
|
|
253
|
+
|
|
254
|
+
for root, dirs, files in os.walk(self.base_dir):
|
|
255
|
+
for file in files:
|
|
256
|
+
rel_path = os.path.relpath(os.path.join(root, file), start=self.base_dir)
|
|
257
|
+
archive.write(os.path.join(root, file), arcname=rel_path)
|
|
258
|
+
|
|
259
|
+
archive.close()
|
|
260
|
+
|
|
261
|
+
def finalize(self, to_file, remove_dir=True):
|
|
262
|
+
# must have at least one sheet
|
|
263
|
+
if not self.sheets:
|
|
264
|
+
self.add_sheet("Sheet1")
|
|
265
|
+
|
|
266
|
+
self._create_content_types()
|
|
267
|
+
self._create_root_rels()
|
|
268
|
+
self._create_app_rels()
|
|
269
|
+
self._create_styles()
|
|
270
|
+
self._create_workbook()
|
|
271
|
+
|
|
272
|
+
for sheet in self.sheets:
|
|
273
|
+
sheet.finalize()
|
|
274
|
+
|
|
275
|
+
self._archive_dir(to_file)
|
|
276
|
+
|
|
277
|
+
if remove_dir:
|
|
278
|
+
shutil.rmtree(self.base_dir)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xlsxlite
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Lightweight XLSX writer with emphasis on minimizing memory usage.
|
|
5
|
+
Project-URL: repository, http://github.com/nyaruka/xlsxlite
|
|
6
|
+
Author-email: TextIt <code@textit.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# XLSXLite
|
|
17
|
+
|
|
18
|
+
[](https://github.com/nyaruka/xlsxlite/actions?query=workflow%3ACI)
|
|
19
|
+
[](https://codecov.io/gh/nyaruka/xlsxlite)
|
|
20
|
+
[](https://pypi.python.org/pypi/xlsxlite/)
|
|
21
|
+
|
|
22
|
+
This is a lightweight XLSX writer with emphasis on minimizing memory usage. It's also really fast.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from xlsxlite.writer import XLSXBook
|
|
26
|
+
book = XLSXBook()
|
|
27
|
+
sheet1 = book.add_sheet("People")
|
|
28
|
+
sheet1.append_row("Name", "Email", "Age")
|
|
29
|
+
sheet1.append_row("Jim", "jim@acme.com", 45)
|
|
30
|
+
book.finalize(to_file="simple.xlsx")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Benchmarks
|
|
34
|
+
|
|
35
|
+
The [benchmarking test](https://github.com/nyaruka/xlsxlite/blob/main/xlsxlite/test/test_perf.py) writes
|
|
36
|
+
rows with 10 cells of random string data to a single sheet workbook. The table below gives the times in seconds (lower is better)
|
|
37
|
+
to write a spreadsheet with the given number of rows, and includes [xlxswriter](https://xlsxwriter.readthedocs.io/) and
|
|
38
|
+
[openpyxl](https://openpyxl.readthedocs.io/) for comparison.
|
|
39
|
+
|
|
40
|
+
Implementation | 100,000 rows | 1,000,000 rows
|
|
41
|
+
----------------|--------------|---------------
|
|
42
|
+
openpyxl | 43.5 | 469.1
|
|
43
|
+
openpyxl + lxml | 21.1 | 226.3
|
|
44
|
+
xlsxwriter | 17.2 | 186.2
|
|
45
|
+
xlsxlite | 1.9 | 19.2
|
|
46
|
+
|
|
47
|
+
## Limitations
|
|
48
|
+
|
|
49
|
+
This library is for projects which need to generate large spreadsheets, quickly, for the purposes of data exchange, and
|
|
50
|
+
so it intentionally only supports a tiny subset of SpreadsheetML specification:
|
|
51
|
+
|
|
52
|
+
* No styling or themes
|
|
53
|
+
* Only strings, numbers, booleans and dates are supported cell types
|
|
54
|
+
|
|
55
|
+
If you need to do anything fancier then take a look at [xlxswriter](https://xlsxwriter.readthedocs.io/) and
|
|
56
|
+
[openpyxl](https://openpyxl.readthedocs.io/).
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
To run all tests:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
uv run pytest xlsxlite -s
|
|
64
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
xlsxlite/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
xlsxlite/utils.py,sha256=5S36PdU-FnCQ3lY9BkCE4whMMxonYrRweuEflxHacC0,378
|
|
3
|
+
xlsxlite/writer.py,sha256=1TjFfBBH-DB3TknNWwVmGdWfUGChIleIEF3UNKBwHik,9345
|
|
4
|
+
xlsxlite/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
xlsxlite/test/base.py,sha256=eLVJSX2Y6hdSMhV6HTOe8rGXa6QGzQ-FKUOkNRkk9F4,1708
|
|
6
|
+
xlsxlite/test/test_perf.py,sha256=T7n7CYzISpictXCj5euSA7A1RnuZu60F2-j6wQ71yq0,1965
|
|
7
|
+
xlsxlite/test/test_utils.py,sha256=0dTObD-BXcfOM8-eUjFDayQBLXgplg8jo6237dvu6Xs,447
|
|
8
|
+
xlsxlite/test/test_writer.py,sha256=L4SWbnNkarIL4dV3Prs7VgN3u983E5htaNIh7ZXv46Q,2857
|
|
9
|
+
xlsxlite-1.1.0.dist-info/METADATA,sha256=3s4Apo56b4rHD7U_0-cFFEXzDHWoBOD_S61X8NikSkE,2446
|
|
10
|
+
xlsxlite-1.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
+
xlsxlite-1.1.0.dist-info/licenses/LICENSE,sha256=noCRz7EAyVA_li4teSPmicKInOy2mgCDR6u4ftxfVgo,1078
|
|
12
|
+
xlsxlite-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2022 TextIt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|