split_ods_links: New tool.
See docstring—this is mostly a post-filter to improve Excel compatibility.
This commit is contained in:
parent
da056917bf
commit
3219bf89d2
2 changed files with 140 additions and 1 deletions
138
conservancy_beancount/tools/split_ods_links.py
Normal file
138
conservancy_beancount/tools/split_ods_links.py
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""split_ods_links.py - Rewrite an ODS to have at most one link per cell
|
||||||
|
|
||||||
|
This is useful when you plan to send the spreadsheet to an Excel user, which
|
||||||
|
only supports one link per cell.
|
||||||
|
"""
|
||||||
|
# Copyright © 2020 Brett Smith
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import concurrent.futures as futmod
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from zipfile import BadZipFile
|
||||||
|
|
||||||
|
import odf.opendocument # type:ignore[import]
|
||||||
|
import odf.table # type:ignore[import]
|
||||||
|
import odf.text # type:ignore[import]
|
||||||
|
|
||||||
|
from ..reports.core import BaseODS
|
||||||
|
|
||||||
|
from typing import (
|
||||||
|
Iterator,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
TextIO,
|
||||||
|
Tuple,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .. import cliutil
|
||||||
|
|
||||||
|
PROGNAME = 'split-ods-links'
|
||||||
|
logger = logging.getLogger('conservancy_beancount.tools.split_ods_links')
|
||||||
|
|
||||||
|
class ODS(BaseODS[Tuple[None], None]):
|
||||||
|
def __init__(self, ods_path: Path) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.document = odf.opendocument.load(ods_path)
|
||||||
|
self.dirty = False
|
||||||
|
|
||||||
|
def section_key(self, row: Tuple[None]) -> None:
|
||||||
|
raise NotImplementedError("split_ods_links.ODS.section_key")
|
||||||
|
|
||||||
|
def split_row_cells(self, row: odf.table.TableRow, count: int) -> Iterator[odf.table.TableRow]:
|
||||||
|
for row_index in range(count):
|
||||||
|
new_row = self.copy_element(row)
|
||||||
|
for cell_index, cell in enumerate(new_row.childNodes):
|
||||||
|
try:
|
||||||
|
cell.childNodes = [cell.childNodes[row_index]]
|
||||||
|
except IndexError:
|
||||||
|
new_row.childNodes[cell_index] = odf.table.TableCell()
|
||||||
|
yield new_row
|
||||||
|
|
||||||
|
def split_link_cells(self) -> None:
|
||||||
|
for sheet in self.document.spreadsheet.getElementsByType(odf.table.Table):
|
||||||
|
for row in sheet.getElementsByType(odf.table.TableRow):
|
||||||
|
cells = row.getElementsByType(odf.table.TableCell)
|
||||||
|
child_counts = [len(cell.childNodes) for cell in cells]
|
||||||
|
link_counts = [len(cell.getElementsByType(odf.text.A)) for cell in cells]
|
||||||
|
if any(count > 1 for count in link_counts):
|
||||||
|
for new_row in self.split_row_cells(row, max(child_counts)):
|
||||||
|
sheet.insertBefore(new_row, row)
|
||||||
|
sheet.removeChild(row)
|
||||||
|
self.dirty = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run_split(cls, path: Path, suffix: str) -> bool:
|
||||||
|
ods = cls(path)
|
||||||
|
ods.split_link_cells()
|
||||||
|
if ods.dirty:
|
||||||
|
out_path = path.with_name(path.name.replace('.', f'{suffix}.', 1))
|
||||||
|
ods.save_path(out_path)
|
||||||
|
return ods.dirty
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(prog=PROGNAME)
|
||||||
|
cliutil.add_version_argument(parser)
|
||||||
|
cliutil.add_loglevel_argument(parser)
|
||||||
|
cliutil.add_jobs_argument(parser)
|
||||||
|
parser.add_argument(
|
||||||
|
'--suffix', '-s',
|
||||||
|
default='_split',
|
||||||
|
help="""Suffix to add to filenames for modified spreadsheets.
|
||||||
|
Pass an empty string argument to overwrite the original spreadsheet.
|
||||||
|
Default %(default)r.
|
||||||
|
""")
|
||||||
|
parser.add_argument(
|
||||||
|
'ods_paths',
|
||||||
|
metavar='ODS_PATH',
|
||||||
|
type=Path,
|
||||||
|
nargs=argparse.ONE_OR_MORE,
|
||||||
|
help="""ODS file(s) to split links in
|
||||||
|
""")
|
||||||
|
return parser.parse_args(arglist)
|
||||||
|
|
||||||
|
def main(arglist: Optional[Sequence[str]]=None,
|
||||||
|
stdout: TextIO=sys.stdout,
|
||||||
|
stderr: TextIO=sys.stderr,
|
||||||
|
) -> int:
|
||||||
|
args = parse_arguments(arglist)
|
||||||
|
cliutil.set_loglevel(logger, args.loglevel)
|
||||||
|
args.ods_paths.sort(key=lambda path: path.stat().st_size, reverse=True)
|
||||||
|
|
||||||
|
returncode = 0
|
||||||
|
max_procs = max(1, min(args.jobs, len(args.ods_paths)))
|
||||||
|
with futmod.ProcessPoolExecutor(max_procs) as pool:
|
||||||
|
procs = {pool.submit(ODS.run_split, path, args.suffix) for path in args.ods_paths}
|
||||||
|
for ods_path, proc in zip(args.ods_paths, procs):
|
||||||
|
try:
|
||||||
|
proc.result()
|
||||||
|
except IOError as error:
|
||||||
|
logger.error("error reading %s: %s", ods_path, error.strerror)
|
||||||
|
returncode = os.EX_DATAERR
|
||||||
|
except BadZipFile as error:
|
||||||
|
logger.error("error parsing %s: %s", ods_path, error.args[0])
|
||||||
|
returncode = os.EX_DATAERR
|
||||||
|
return returncode
|
||||||
|
|
||||||
|
entry_point = cliutil.make_entry_point(__name__, PROGNAME)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit(entry_point())
|
3
setup.py
3
setup.py
|
@ -5,7 +5,7 @@ from setuptools import setup
|
||||||
setup(
|
setup(
|
||||||
name='conservancy_beancount',
|
name='conservancy_beancount',
|
||||||
description="Plugin, library, and reports for reading Conservancy's books",
|
description="Plugin, library, and reports for reading Conservancy's books",
|
||||||
version='1.9.7',
|
version='1.10.0',
|
||||||
author='Software Freedom Conservancy',
|
author='Software Freedom Conservancy',
|
||||||
author_email='info@sfconservancy.org',
|
author_email='info@sfconservancy.org',
|
||||||
license='GNU AGPLv3+',
|
license='GNU AGPLv3+',
|
||||||
|
@ -44,6 +44,7 @@ setup(
|
||||||
'fund-report = conservancy_beancount.reports.fund:entry_point',
|
'fund-report = conservancy_beancount.reports.fund:entry_point',
|
||||||
'ledger-report = conservancy_beancount.reports.ledger:entry_point',
|
'ledger-report = conservancy_beancount.reports.ledger:entry_point',
|
||||||
'opening-balances = conservancy_beancount.tools.opening_balances:entry_point',
|
'opening-balances = conservancy_beancount.tools.opening_balances:entry_point',
|
||||||
|
'split-ods-links = conservancy_beancount.tools.split_ods_links:entry_point',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue