commit 42fa1a8dfeaa3dd88822f71843b0f79f73058987 Author: Alexander Rossmanith Date: Fri Aug 7 20:30:33 2026 +0530 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7faf40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,207 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..892a65b --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# exhibittool + +A command-line tool that reads an ODT document, extracts the table of exhibits, and creates a front page for each exhibit. + +The exhibit table must be a two-column table of exhibits. the first column contains the exhibit label, e.g. "Exhibt R-1". The second column contains the exhibit description, e.g. "Sale deed no 123". diff --git a/data/example.odt b/data/example.odt new file mode 100644 index 0000000..e372f36 Binary files /dev/null and b/data/example.odt differ diff --git a/exhibit-frontpages.odt b/exhibit-frontpages.odt new file mode 100644 index 0000000..c48e066 Binary files /dev/null and b/exhibit-frontpages.odt differ diff --git a/exhibittool.code-workspace b/exhibittool.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/exhibittool.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c50ae3c --- /dev/null +++ b/poetry.lock @@ -0,0 +1,114 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "odfpy" +version = "1.4.1" +description = "Python API and tools to manipulate OpenDocument files" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "odfpy-1.4.1.tar.gz", hash = "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec"}, +] + +[package.dependencies] +defusedxml = "*" + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "ruff" +version = "0.14.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1"}, + {file = "ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11"}, + {file = "ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3"}, + {file = "ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3"}, + {file = "ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8"}, + {file = "ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839"}, + {file = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7"}, + {file = "ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc"}, + {file = "ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a"}, + {file = "ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096"}, + {file = "ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df"}, + {file = "ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05"}, + {file = "ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5"}, + {file = "ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e"}, + {file = "ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770"}, + {file = "ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9"}, + {file = "ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af"}, + {file = "ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a"}, + {file = "ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96"}, +] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.13" +content-hash = "d08e22ac9c2eed536f2987c53d969e464ba23c20880c3cbaaa42225738bdd9cf" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..329c150 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "exhibittool" +version = "0.1.0" +description = "Generate a front page for all exhibits" +authors = [ + {name = "Alexander Rossmanith", email = "exhibittool@alexander-rossmanith.de"} +] +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "odfpy (>=1.4.1,<2.0.0)" +] + +[tool.poetry] +package-mode = false +packages = [{include = "exhibittool", from = "src"}] + +[tool.poetry.group.dev.dependencies] +flake8 = "^7.3.0" +ruff = "^0.14.2" + +[tool.flake8] +max-line-length = 99 + +[tool.ruff] +line-length = 99 + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +docstring-code-format = true +docstring-code-line-length = 79 + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/scripts/Install-Python.ps1 b/scripts/Install-Python.ps1 new file mode 100644 index 0000000..9987f24 --- /dev/null +++ b/scripts/Install-Python.ps1 @@ -0,0 +1,3 @@ +scoop install python +scoop install pipx +pipx install poetry diff --git a/scripts/Install-Scoop.ps1 b/scripts/Install-Scoop.ps1 new file mode 100644 index 0000000..e5594f6 --- /dev/null +++ b/scripts/Install-Scoop.ps1 @@ -0,0 +1,2 @@ +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression diff --git a/src/exhibittool/__init__.py b/src/exhibittool/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/exhibittool/main.py b/src/exhibittool/main.py new file mode 100644 index 0000000..ff3d419 --- /dev/null +++ b/src/exhibittool/main.py @@ -0,0 +1,97 @@ +import argparse +from odf.opendocument import load, OpenDocumentText +from odf.table import Table, TableRow, TableCell +from odf.text import P +from odf.style import Style, TextProperties, ParagraphProperties + +DEFAULT_TITLE = ( + "Munsiff Court, Adimaly, OS No. 92 of 2024, " + "Written Statement 5. 11. 2025" +) + + +def create_odt_front_pages(exhibits, case_title, output_file): + textdoc = OpenDocumentText() + + # Case-title style: large, centered, sans-serif, with spacing + case_title_style = Style(name="CaseTitleStyle", family="paragraph") + case_title_style.addElement( + TextProperties(fontsize="18pt", fontfamily="Liberation Sans") + ) + case_title_style.addElement( + ParagraphProperties(textalign="center", marginbottom="1cm") + ) + textdoc.styles.addElement(case_title_style) + + # Exhibit-label style: large, centered, sans-serif, with spacing + exhibit_label_style = Style(name="ExhibitLabel", family="paragraph") + exhibit_label_style.addElement( + TextProperties(fontsize="24pt", fontfamily="Liberation Sans") + ) + exhibit_label_style.addElement( + ParagraphProperties(textalign="center", marginbottom="1cm") + ) + textdoc.styles.addElement(exhibit_label_style) + + # Description style: smaller, left-aligned, sans-serif, with spacing + desc_style = Style(name="DescStyle", family="paragraph") + desc_style.addElement(TextProperties(fontsize="18pt", fontfamily="Liberation Sans")) + desc_style.addElement(ParagraphProperties(textalign="start", marginbottom="1cm")) + textdoc.styles.addElement(desc_style) + + # Page break style + pagebreak_style = Style(name="PageBreak", family="paragraph") + pagebreak_style.addElement(ParagraphProperties(breakbefore="page")) + textdoc.styles.addElement(pagebreak_style) + + for i, (label, description) in enumerate(exhibits): + para = P(stylename=case_title_style, text=case_title) + textdoc.text.addElement(para) + + para = P(stylename=exhibit_label_style, text=label) + textdoc.text.addElement(para) + + desc_para = P(stylename=desc_style) + desc_para.addText(f"Description: {description}") + textdoc.text.addElement(desc_para) + + if i < len(exhibits) - 1: + textdoc.text.addElement(P(stylename=pagebreak_style)) + + textdoc.save(output_file) + + +def extract_exhibits(odt_path): + doc = load(odt_path) + exhibits = [] + + for table in doc.getElementsByType(Table): + for row in table.getElementsByType(TableRow): + cells = row.getElementsByType(TableCell) + if len(cells) >= 2: + label = "".join([str(p) for p in cells[0].getElementsByType(P)]).strip() + desc = "".join([str(p) for p in cells[1].getElementsByType(P)]).strip() + if label and desc: + exhibits.append((label, desc)) + return exhibits + + +def main(): + parser = argparse.ArgumentParser( + description="Generate a single ODT front page document for all exhibits." + ) + parser.add_argument("input_file", help="Path to the input ODT file") + parser.add_argument("output_file", help="Path to the output ODT file") + parser.add_argument( + "--case-title", default=DEFAULT_TITLE, help="Title of the court case to display" + ) + + args = parser.parse_args() + + exhibits = extract_exhibits(args.input_file) + create_odt_front_pages(exhibits, args.case_title, args.output_file) + print(f"Saved all front pages to: {args.output_file}") + + +if __name__ == "__main__": + main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29