From 11746fc56e0853acc5217c93c30af42988123a58 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 27 Oct 2023 20:18:03 +0200 Subject: [PATCH] Fixes #567 - Implement ods import --- HISTORY.md | 2 + docs/formats.rst | 11 +++- src/tablib/formats/_ods.py | 100 +++++++++++++++++++++++++++++ tests/files/book.ods | Bin 0 -> 8833 bytes tests/files/ragged.ods | Bin 0 -> 9859 bytes tests/files/unknown_value_type.ods | Bin 0 -> 7756 bytes tests/test_tablib.py | 51 +++++++++++++-- 7 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 tests/files/book.ods create mode 100644 tests/files/ragged.ods create mode 100644 tests/files/unknown_value_type.ods diff --git a/HISTORY.md b/HISTORY.md index 2c1e59e8..17551004 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,8 @@ ### Improvements - The html format now supports importing from HTML content (#243) +- The ODS format now supports importing from .ods files (#567). The support is + still a bit experimental. ### Changes diff --git a/docs/formats.rst b/docs/formats.rst index 636f7309..178edbda 100644 --- a/docs/formats.rst +++ b/docs/formats.rst @@ -145,12 +145,19 @@ If a title has been set, it will be exported as the table caption. ods === -Export data in OpenDocument Spreadsheet format. The ``ods`` format is currently -export-only. +Import/export data in OpenDocument Spreadsheet format. + +.. versionadded:: 3.6.0 + + Import functionality was added. This format is optional, install Tablib with ``pip install "tablib[ods]"`` to make the format available. +The ``import_set()`` method also supports a ``skip_lines`` parameter that you +can set to a number of lines that should be skipped before starting to read +data. + .. admonition:: Binary Warning :class:`Dataset.ods` contains binary data, so make sure to write in binary mode:: diff --git a/src/tablib/formats/_ods.py b/src/tablib/formats/_ods.py index ec618f67..346c5acc 100644 --- a/src/tablib/formats/_ods.py +++ b/src/tablib/formats/_ods.py @@ -2,10 +2,13 @@ """ import numbers +from datetime import date, datetime, time from io import BytesIO from odf import opendocument, style, table, text +import tablib + bold = style.Style(name="bold", family="paragraph") bold.addElement(style.TextProperties( fontweight="bold", @@ -49,6 +52,95 @@ def export_book(cls, databook): wb.save(stream) return stream.getvalue() + @classmethod + def import_sheet(cls, dset, sheet, headers=True, skip_lines=0): + """Populate dataset `dset` with sheet data.""" + + dset.title = sheet.getAttribute('name') + + def is_real_cell(cell): + return cell.hasChildNodes() or not cell.getAttribute('numbercolumnsrepeated') + + for i, row in enumerate(sheet.childNodes): + if row.tagName != 'table:table-row': + continue + if i < skip_lines: + continue + row_vals = [cls.read_cell(cell) for cell in row.childNodes if is_real_cell(cell)] + if not row_vals: + continue + if i == skip_lines and headers: + dset.headers = row_vals + else: + if i > skip_lines and len(row_vals) < dset.width: + row_vals += [''] * (dset.width - len(row_vals)) + dset.append(row_vals) + + @classmethod + def read_cell(cls, cell, value_type=None): + def convert_date(val): + if 'T' in val: + return datetime.strptime(val, "%Y-%m-%dT%H:%M:%S") + else: + return datetime.strptime(val, "%Y-%m-%d").date() + + if value_type is None: + value_type = cell.getAttribute('valuetype') + if value_type == 'date': + date_value = cell.getAttribute('datevalue') + if date_value: + return convert_date(date_value) + if value_type == 'time': + time_value = cell.getAttribute('timevalue') + return datetime.strptime(time_value, "%H:%M:%S").time() + if value_type == 'boolean': + bool_value = cell.getAttribute('booleanvalue') + return bool_value == 'true' + if not cell.childNodes: + value = getattr(cell, 'data', None) + if value is None: + value = cell.getAttribute('value') + if value is None: + return '' + if value_type == 'float': + return float(value) + if value_type == 'date': + return convert_date(value) + if value_type == 'boolean': + return convert_date(value) + return value # Any other type default to 'string' + + for subnode in cell.childNodes: + value = cls.read_cell(subnode, value_type) + if value: + return value + + @classmethod + def import_set(cls, dset, in_stream, headers=True, skip_lines=0): + """Populate dataset `dset` from ODS stream.""" + + dset.wipe() + + ods_book = opendocument.load(in_stream) + for sheet in ods_book.spreadsheet.childNodes: + if sheet.qname[1] == 'table': + cls.import_sheet(dset, sheet, headers, skip_lines) + + @classmethod + def import_book(cls, dbook, in_stream, headers=True): + """Populate databook `dbook` from ODS stream.""" + + dbook.wipe() + + ods_book = opendocument.load(in_stream) + + for sheet in ods_book.spreadsheet.childNodes: + if sheet.qname[1] != 'table': + continue + dset = tablib.Dataset() + cls.import_sheet(dset, sheet, headers) + dbook.add_sheet(dset) + @classmethod def dset_sheet(cls, dataset, ws): """Completes given worksheet from given Dataset.""" @@ -66,6 +158,14 @@ def dset_sheet(cls, dataset, ws): for j, col in enumerate(row): if isinstance(col, numbers.Number): cell = table.TableCell(valuetype="float", value=col) + elif isinstance(col, datetime): + cell = table.TableCell( + valuetype="date", value=col.strftime('%Y-%m-%dT%H:%M:%S') + ) + elif isinstance(col, date): + cell = table.TableCell(valuetype="date", datevalue=col.strftime('%Y-%m-%d')) + elif isinstance(col, time): + cell = table.TableCell(valuetype="time", timevalue=col.strftime('%H:%M:%S')) else: cell = table.TableCell(valuetype="string") cell.addElement(text.P(text=str(col), stylename=style)) diff --git a/tests/files/book.ods b/tests/files/book.ods new file mode 100644 index 0000000000000000000000000000000000000000..a26976805478cf9a8dbd28f51acd15fe94a43162 GIT binary patch literal 8833 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 E0fVp6TmS$7 literal 0 HcmV?d00001 diff --git a/tests/files/ragged.ods b/tests/files/ragged.ods new file mode 100644 index 0000000000000000000000000000000000000000..e9e86c87426cdb606124f6f1eef2c1a4171246d3 GIT binary patch literal 9859 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 literal 0 HcmV?d00001 diff --git a/tests/test_tablib.py b/tests/test_tablib.py index 47659f84..01daae76 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -1107,13 +1107,52 @@ def test_tsv_export(self): class ODSTests(BaseTestCase): - def test_ods_export_datatypes(self): + def test_ods_export_import_set(self): + date = datetime.date(2019, 10, 4) date_time = datetime.datetime(2019, 10, 4, 12, 30, 8) - data.append(('string', '004', 42, 21.55, Decimal('34.5'), date_time)) - data.headers = ('string', 'start0', 'integer', 'float', 'decimal', 'date/time') - # ODS is currently write-only, just test that output doesn't crash. - assert data.ods is not None - assert len(data.ods) + time = datetime.time(14, 30) + data.append(('string', '004', 42, 21.55, Decimal('34.5'), date, time, date_time)) + data.headers = ( + 'string', 'start0', 'integer', 'float', 'decimal', 'date', 'time', 'date/time' + ) + _ods = data.ods + data.ods = _ods + self.assertEqual(data.dict[0]['string'], 'string') + self.assertEqual(data.dict[0]['start0'], '004') + self.assertEqual(data.dict[0]['integer'], 42) + self.assertEqual(data.dict[0]['float'], 21.55) + self.assertEqual(data.dict[0]['decimal'], 34.5) + self.assertEqual(data.dict[0]['date'], date) + self.assertEqual(data.dict[0]['time'], time) + self.assertEqual(data.dict[0]['date/time'], date_time) + + def test_ods_import_book(self): + ods_source = Path(__file__).parent / 'files' / 'book.ods' + with ods_source.open('rb') as fh: + dbook = tablib.Databook().load(fh, 'ods') + self.assertEqual(len(dbook.sheets()), 2) + + def test_ods_import_set_skip_lines(self): + data.append(('garbage', 'line', '')) + data.append(('', '', '')) + data.append(('id', 'name', 'description')) + _ods = data.ods + new_data = tablib.Dataset().load(_ods, skip_lines=2) + self.assertEqual(new_data.headers, ['id', 'name', 'description']) + + def test_ods_import_set_ragged(self): + ods_source = Path(__file__).parent / 'files' / 'ragged.ods' + with ods_source.open('rb') as fh: + dataset = tablib.Dataset().load(fh, 'ods') + self.assertEqual(dataset.pop(), (1, '', True, '')) + + def test_ods_unknown_value_type(self): + # The ods file was trafficked to contain: + # + ods_source = Path(__file__).parent / 'files' / 'unknown_value_type.ods' + with ods_source.open('rb') as fh: + dataset = tablib.Dataset().load(fh, 'ods') + self.assertEqual(dataset.pop(), ('abcd',)) class XLSTests(BaseTestCase):