Compare commits

..

97 Commits

Author SHA1 Message Date
miklo 4d46082b47 .fods parser correction for hyperlinks 2026-03-10 00:41:00 +00:00
dependabot[bot] 26b95141dc 💚 ci(deps): Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 19:14:11 +01:00
dependabot[bot] ccc376b646 ⬆️ dep-bump(deps): Bump lxml from 6.0.0 to 6.0.2
Bumps [lxml](https://github.com/lxml/lxml) from 6.0.0 to 6.0.2.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-6.0.0...lxml-6.0.2)

---
updated-dependencies:
- dependency-name: lxml
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 19:10:29 +01:00
dependabot[bot] 07fd1630ed ⬆️ dep-bump(deps-dev): Bump commitizen from 4.8.3 to 4.9.1
Bumps [commitizen](https://github.com/commitizen-tools/commitizen) from 4.8.3 to 4.9.1.
- [Release notes](https://github.com/commitizen-tools/commitizen/releases)
- [Changelog](https://github.com/commitizen-tools/commitizen/blob/master/CHANGELOG.md)
- [Commits](https://github.com/commitizen-tools/commitizen/compare/v4.8.3...v4.9.1)

---
updated-dependencies:
- dependency-name: commitizen
  dependency-version: 4.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 19:07:45 +01:00
dependabot[bot] e482b7eab7 ⬆️ dep-bump(deps-dev): Bump pytest-cov from 6.2.1 to 7.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.2.1 to 7.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.2.1...v7.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 7.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 19:01:02 +01:00
dependabot[bot] f915d509c0 ⬆️ dep-bump(deps-dev): Bump pytest from 8.4.1 to 8.4.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.1 to 8.4.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.1...8.4.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 18:57:01 +01:00
dependabot[bot] 53693f3173 💚 ci(deps): Bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 18:52:20 +01:00
dependabot[bot] b4a24c672c ⬆️ dep-bump(deps-dev): Bump types-lxml
Bumps [types-lxml](https://github.com/abelcheung/types-lxml) from 2025.3.30 to 2025.8.25.
- [Release notes](https://github.com/abelcheung/types-lxml/releases)
- [Commits](https://github.com/abelcheung/types-lxml/compare/2025.03.30...2025.08.25)

---
updated-dependencies:
- dependency-name: types-lxml
  dependency-version: 2025.8.25
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 18:51:48 +01:00
dependabot[bot] 175dfac483 ⬆️ dep-bump(deps): Bump lxml from 5.4.0 to 6.0.0
Bumps [lxml](https://github.com/lxml/lxml) from 5.4.0 to 6.0.0.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-5.4.0...lxml-6.0.0)

---
updated-dependencies:
- dependency-name: lxml
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-24 22:12:09 +02:00
dependabot[bot] eb4c5e599a ⬆️ dep-bump(deps-dev): Bump flake8 from 7.2.0 to 7.3.0
Bumps [flake8](https://github.com/pycqa/flake8) from 7.2.0 to 7.3.0.
- [Commits](https://github.com/pycqa/flake8/compare/7.2.0...7.3.0)

---
updated-dependencies:
- dependency-name: flake8
  dependency-version: 7.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-24 22:11:26 +02:00
dependabot[bot] c068e5b711 ⬆️ dep-bump(deps-dev): Bump mypy from 1.16.1 to 1.17.1
Bumps [mypy](https://github.com/python/mypy) from 1.16.1 to 1.17.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.16.1...v1.17.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.17.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-24 22:11:17 +02:00
dependabot[bot] 40a32ac2e3 ⬆️ dep-bump(deps-dev): Bump pre-commit from 4.2.0 to 4.3.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-24 22:11:04 +02:00
dependabot[bot] ed28f38fa7 💚 ci(deps): Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-24 22:10:26 +02:00
dependabot[bot] d51227389d ⬆️ dep-bump(deps): Bump pandas from 2.3.0 to 2.3.2
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.3.0 to 2.3.2.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.3.0...v2.3.2)

---
updated-dependencies:
- dependency-name: pandas
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-24 22:09:47 +02:00
dependabot[bot] 8d9c9a5d17 ⬆️ dep-bump(deps-dev): Bump pytest from 8.4.0 to 8.4.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.0 to 8.4.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.0...8.4.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-22 16:15:13 +02:00
dependabot[bot] 830e97dc0e ⬆️ dep-bump(deps-dev): Bump mypy from 1.16.0 to 1.16.1
Bumps [mypy](https://github.com/python/mypy) from 1.16.0 to 1.16.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.16.0...v1.16.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.16.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-22 13:13:56 +02:00
dependabot[bot] 4f694224e1 ⬆️ dep-bump(deps-dev): Bump pytest-cov from 6.1.1 to 6.2.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.1.1 to 6.2.1.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.1.1...v6.2.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 01:27:09 +02:00
dependabot[bot] db60d45c3f ⬆️ dep-bump(deps-dev): Bump commitizen from 4.8.2 to 4.8.3
Bumps [commitizen](https://github.com/commitizen-tools/commitizen) from 4.8.2 to 4.8.3.
- [Release notes](https://github.com/commitizen-tools/commitizen/releases)
- [Changelog](https://github.com/commitizen-tools/commitizen/blob/master/CHANGELOG.md)
- [Commits](https://github.com/commitizen-tools/commitizen/compare/v4.8.2...v4.8.3)

---
updated-dependencies:
- dependency-name: commitizen
  dependency-version: 4.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 01:18:33 +02:00
dependabot[bot] 2f64149e4c ⬆️ dep-bump(deps): Bump pandas from 2.2.3 to 2.3.0
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.2.3 to 2.3.0.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.2.3...v2.3.0)

---
updated-dependencies:
- dependency-name: pandas
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-07 02:30:59 +02:00
dependabot[bot] aff079bc81 ⬆️ dep-bump(deps-dev): Bump pytest from 8.3.5 to 8.4.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.5 to 8.4.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.5...8.4.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 19:30:36 +02:00
dependabot[bot] 2344208d44 ⬆️ dep-bump(deps-dev): Bump mypy from 1.15.0 to 1.16.0
Bumps [mypy](https://github.com/python/mypy) from 1.15.0 to 1.16.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.16.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 19:26:44 +02:00
dependabot[bot] fce95599f6 ⬆️ dep-bump(deps-dev): Bump commitizen from 4.6.3 to 4.8.2
Bumps [commitizen](https://github.com/commitizen-tools/commitizen) from 4.6.3 to 4.8.2.
- [Release notes](https://github.com/commitizen-tools/commitizen/releases)
- [Changelog](https://github.com/commitizen-tools/commitizen/blob/master/CHANGELOG.md)
- [Commits](https://github.com/commitizen-tools/commitizen/compare/v4.6.3...v4.8.2)

---
updated-dependencies:
- dependency-name: commitizen
  dependency-version: 4.8.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 19:21:02 +02:00
dependabot[bot] 7c58c1e276 💚 ci(deps): Bump abatilo/actions-poetry from 3 to 4
Bumps [abatilo/actions-poetry](https://github.com/abatilo/actions-poetry) from 3 to 4.
- [Release notes](https://github.com/abatilo/actions-poetry/releases)
- [Changelog](https://github.com/abatilo/actions-poetry/blob/master/.releaserc)
- [Commits](https://github.com/abatilo/actions-poetry/compare/v3...v4)

---
updated-dependencies:
- dependency-name: abatilo/actions-poetry
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 17:55:24 +02:00
ljnsn b5fa599242
bump: version 1.0.1 → 1.0.2 2025-05-09 02:28:59 +02:00
ljnsn 7edb462783
config(commitizen): use version provider 2025-05-09 02:28:53 +02:00
ljnsn 49d9524c63 chore: add py.typed 2025-05-09 02:25:48 +02:00
ljnsn 8de30a76cd fix: bump all dependencies 2025-05-09 02:25:48 +02:00
ljnsn 6334e4e664 fix: only get path once 2025-05-09 02:23:19 +02:00
ljnsn 361b99a265 ci: install build dependencies 2025-05-09 02:14:52 +02:00
ljnsn dada48559e ci: add tests for 3.13 2025-05-09 02:14:52 +02:00
ljnsn 5d07827a8d fix: bump pandas 2025-05-09 02:14:52 +02:00
ljnsn 0ce4b2942d chore: require python <4 2025-05-09 02:14:52 +02:00
dependabot[bot] 913af3fb27 Bump jinja2 from 3.1.4 to 3.1.6
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.4 to 3.1.6.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.4...3.1.6)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 01:50:04 +02:00
ljnsn ec6c857cdc chore: ignore local mise toml 2025-05-09 01:40:31 +02:00
ljnsn fa02cf27c4 ci: use poetry action and update 2025-05-09 01:38:37 +02:00
dependabot[bot] a2a999a42b 💚 ci(deps): Bump actions/cache from 2 to 4
Bumps [actions/cache](https://github.com/actions/cache) from 2 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 01:19:25 +02:00
dependabot[bot] f415d06af3 💚 ci(deps): Bump actions/checkout from 2 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 01:18:32 +02:00
dependabot[bot] 955677c538 💚 ci(deps): Bump actions/setup-python from 2 to 5
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 01:18:15 +02:00
ljnsn 4a461c0026 ci: add dependabot 2025-05-09 01:15:11 +02:00
dependabot[bot] a4b271370e Bump virtualenv from 20.26.2 to 20.26.6
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.26.2 to 20.26.6.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.26.2...20.26.6)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 09:46:45 +01:00
dependabot[bot] d5b1acc0f8 Bump zipp from 3.19.0 to 3.19.1
Bumps [zipp](https://github.com/jaraco/zipp) from 3.19.0 to 3.19.1.
- [Release notes](https://github.com/jaraco/zipp/releases)
- [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst)
- [Commits](https://github.com/jaraco/zipp/compare/v3.19.0...v3.19.1)

---
updated-dependencies:
- dependency-name: zipp
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-10 11:55:47 +02:00
ljnsn 544329180b
bump: version 1.0.0 → 1.0.1 2024-05-31 23:47:48 +02:00
ljnsn 28f61d86bd
fix: support py312 2024-05-31 23:45:19 +02:00
ljnsn 01e5c45fc3
bump: version 0.2.0 → 1.0.0 2024-05-31 17:57:13 +02:00
ljnsn a9ed587ac0 fix-lint: black format 2024-05-31 17:55:59 +02:00
ljnsn 8cd70f96a2 ci: drop tests on 3.8 and add 3.11 and 3.12 2024-05-31 17:55:59 +02:00
ljnsn 5c5a1871f1 fix: add pre-commit dev dependency 2024-05-31 17:55:59 +02:00
ljnsn 8a9a0bad3d ignore: pyenv python 2024-05-31 17:55:59 +02:00
ljnsn 102d661cdc fix: prod dep bumps 2024-05-31 17:55:59 +02:00
ljnsn ee3062d587 fix!: drop support for python 3.8 2024-05-31 17:55:59 +02:00
ljnsn 375451dff5 fix: dev dependency constraints 2024-05-31 17:55:59 +02:00
dependabot[bot] d1241e83c6 Bump jinja2 from 3.1.2 to 3.1.3
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-13 12:30:53 +01:00
ljnsn f8a9dbe38d bump: version 0.1.4 → 0.2.0 2022-12-21 23:51:45 +01:00
ljnsn c62134f584 fix: remove push hook 2022-12-21 23:51:45 +01:00
ljnsn 3c07c92527 fix: tell commitizen to bump version in pyproject.toml 2022-12-21 23:51:45 +01:00
ljnsn 80da4b133b feat: add commitizen
This is a feature commit, because technically the addition of the
`skiprows` parameter should have bumped the minor version.
2022-12-21 23:51:45 +01:00
ljnsn da041a2d46 No need to pin poetry since release of 1.2.0. 2022-12-21 23:51:45 +01:00
ljnsn 2a04a4fa27 Ignore mypy_cache. 2022-12-21 23:51:45 +01:00
ljnsn 6c61b8c2cc Fix type annotations for 3.8. 2022-12-21 23:51:45 +01:00
ljnsn e6e32f9108 Use typing.Tuple to support 3.8. 2022-12-21 23:51:45 +01:00
ljnsn bf57625864 Run on push to any branch. 2022-12-21 23:51:45 +01:00
ljnsn 5f4cc608f6 Drop tests on Python 3.7 and add 3.10. 2022-12-21 23:51:45 +01:00
ljnsn 8e4fa26733 Ignore ezodf imports. 2022-12-21 23:51:45 +01:00
ljnsn d459a732ca Fix lxml type issues. 2022-12-21 23:51:45 +01:00
ljnsn 1c255f60e7 Add pandas and lxml stubs. 2022-12-21 23:51:45 +01:00
ljnsn d21253541c Add mypy and flake8 dev dependencies. 2022-12-21 23:51:45 +01:00
ljnsn 053a9540b5 Use latest versions. 2022-12-21 23:51:45 +01:00
ljnsn 81e1d33658 Bump dependencies. 2022-12-21 23:51:45 +01:00
ljnsn e20905e992 Add type ignore. 2022-12-21 23:51:45 +01:00
ljnsn de8e688359 Remove outer test class and add typing to tests. 2022-12-21 23:51:45 +01:00
ljnsn aa4e0cd60c Raise error on invalid path. Fixes #5 2022-12-21 23:51:45 +01:00
ljnsn 538120ce82 Add type hints for fods. 2022-12-21 23:51:45 +01:00
ljnsn e9e4b3b272 Add more type hints. 2022-12-21 23:51:45 +01:00
ljnsn 3e96dae284 Pass correct arg type. 2022-12-21 23:51:45 +01:00
ljnsn 64ea7059d2 Add type hints and change docstyle to google. 2022-12-21 23:51:45 +01:00
ljnsn bd3caab7f3 Factor out logic to get columns into separate functions. 2022-12-21 23:51:45 +01:00
ljnsn 9bf4415a9f Improve login in `parse_data`. 2022-12-21 23:51:45 +01:00
ljnsn 5b9ea785e2 Add .coverage to .gitignore. 2022-12-21 23:51:45 +01:00
ljnsn 297e288526 Add `pytest-cov` dev dependency. 2022-12-21 23:51:45 +01:00
ljnsn 936fff3d18 Bump dev dependencies. 2022-12-21 23:51:45 +01:00
ljnsn 7bbd44b4b6 Bump dependencies and drop support for python 3.7 2022-12-21 23:51:45 +01:00
ljnsn 2b87c80b65 Add skiprows parameter to docstring. 2022-11-16 00:41:17 +01:00
ljnsn 5df67a728a Simplify skiprows. We're currently not doing any input validation.
It might make sense to add, but then at the top level function.
2022-11-16 00:41:17 +01:00
ljnsn e88ac035a2 Fix test list assertion. 2022-11-16 00:41:17 +01:00
ljnsn 963c19769e Revert files changed to state at master. 2022-11-16 00:41:17 +01:00
ljnsn 675c002deb Remove example file at top level. 2022-11-16 00:41:17 +01:00
Vagner Bessa e06e2519a0 Add: test to 'skiprow' feature 2022-11-16 00:41:17 +01:00
Vagner Bessa 9dd3950bb9 implement optional 'skiprow' to read_ods. 2022-11-16 00:41:17 +01:00
Marvin Poul 5b4dfb7413 Fix typo 2022-07-13 21:51:12 +02:00
iuvbio 1442db1451 Bump version: 0.1.3 → 0.1.4 2021-10-04 17:50:55 +02:00
iuvbio 47ed598b9e update github workflow 2021-10-04 17:43:44 +02:00
iuvbio 01809a0dbb user importlib_metadata to get version 2021-10-04 17:43:32 +02:00
iuvbio 83011363ba remove old build system files 2021-10-04 17:43:03 +02:00
iuvbio 23cbf83fd3 add pyproject.toml and poetry.lock 2021-10-04 17:42:26 +02:00
iuvbio eb461f7332 add info about which backend is used to docstring 2021-10-04 16:46:48 +02:00
iuvbio c1d003bec4 improve docstring 2021-10-04 14:34:29 +02:00
iuvbio b7113b3011 make ods backend the default 2021-10-04 14:34:14 +02:00
20 changed files with 2712 additions and 236 deletions

22
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,22 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: 💚 ci
include: "scope"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: ⬆️ dep-bump
include: "scope"

View File

@ -2,30 +2,46 @@ name: Lint and test
on: on:
push: push:
branches: [ master ] branches: ["**"]
pull_request: pull_request:
branches: [ master ] branches: [master]
jobs: jobs:
build: test:
name: Test on ${{ matrix.python_version }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
fail-fast: false
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v6
- name: Set up Python 3.9 - name: Set up Python ${{ matrix.python_version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v6
with: with:
python-version: 3.9 python-version: ${{ matrix.python_version }}
- name: Install dependencies - name: Install poetry
run: | uses: abatilo/actions-poetry@v4
python -m pip install --upgrade pip with:
pip install black pytest poetry-version: "2.1.2"
pip install . - name: Configure poetry
- name: Lint with black run: |
run: | poetry config virtualenvs.create true --local
# stop the build if there are Python syntax errors or undefined names poetry config virtualenvs.in-project true --local
black --check --diff pandas_ods_reader/ tests/ - uses: actions/cache@v4
- name: Test with pytest name: Define a cache for the virtual environment based on the dependencies lock file
run: | with:
python -m pytest tests/ path: ./.venv
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('poetry.lock') }}-venv
- name: Install build dependencies
run: sudo apt install -y libxml2-dev libxslt-dev
- name: Install Dependencies
run: poetry install
- name: Lint with black
run: |
# stop the build if there are Python syntax errors or undefined names
poetry run black --check --diff pandas_ods_reader/ tests/
- name: Test with pytest
run: |
poetry run pytest tests/

12
.gitignore vendored
View File

@ -27,3 +27,15 @@ venv/
# vim config # vim config
.vim/ .vim/
# Coverage
.coverage
# mypy
.mypy_cache/
# pyenv
.python-version
# mise
mise.local.toml

5
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,5 @@
repos:
- repo: https://github.com/commitizen-tools/commitizen
rev: v2.38.0
hooks:
- id: commitizen

View File

@ -1,3 +0,0 @@
include LICENSE.txt
include README.md
include pandas_ods_reader/VERSION

View File

@ -1 +0,0 @@
0.1.2

View File

@ -2,14 +2,10 @@ import sys
from .main import read_ods from .main import read_ods
__distname__ = "pandas-ods-reader"
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
import importlib.metadata from importlib.metadata import version
__version__ = importlib.metadata.version(__distname__)
else: else:
import pkg_resources from importlib_metadata import version
__version__ = pkg_resources.get_distribution(__distname__).version
__version__ = version("pandas-ods-reader")

View File

@ -1,66 +1,99 @@
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path
from types import ModuleType
from typing import Any, Dict, Iterator, List, Union
import pandas as pd import pandas as pd
from .utils import sanitize_df from .utils import sanitize_df
def parse_data(backend, rows, headers=True, columns=None): def get_columns_from_headers(backend: ModuleType, row: Any) -> List[str]:
df_dict = OrderedDict() repeat_until = -1
col_index = {} repeat_value = None
# columns as lists in a dictionary
columns = []
# parse the first row as column names
for k, cell in enumerate(row):
value, n_repeated = backend.get_value(cell)
if n_repeated > 0:
repeat_value = value
repeat_until = n_repeated + k
if not value and k <= repeat_until:
value = repeat_value
if k == repeat_until:
# reset to allow for more than one repeated column
repeat_until = -1
if value and value not in columns:
columns.append(value)
else:
column_name = value if value else "unnamed"
# add count to column name
idx = 1
while f"{column_name}.{idx}" in columns:
idx += 1
columns.append(f"{column_name}.{idx}")
return columns
def get_generic_columns(row: Any) -> List[str]:
return [f"column.{j}" for j in range(len(row))]
def get_columns(backend: ModuleType, row: Any, headers: bool) -> List[str]:
if headers:
return get_columns_from_headers(backend, row)
return get_generic_columns(row)
def parse_data(
backend: ModuleType,
rows: Iterator[List[Any]],
headers: bool,
columns: List[str],
skiprows: int,
) -> pd.DataFrame:
df_dict: OrderedDict[str, Any] = OrderedDict()
col_index: Dict[int, str] = {}
for _ in range(skiprows):
next(rows)
for i, row in enumerate(rows): for i, row in enumerate(rows):
# row is a list of cells # row is a list of cells
if headers and i == 0 and not columns:
repeat_until = -1
repeat_value = None
# columns as lists in a dictionary
columns = []
# parse the first row as column names
for k, cell in enumerate(row):
value, n_repeated = backend.get_value(cell)
if n_repeated > 0:
repeat_value = value
repeat_until = n_repeated + k
if not value and k <= repeat_until:
value = repeat_value
if k == repeat_until:
# reset to allow for more than one repeated column
repeat_until = -1
if value and value not in columns:
columns.append(value)
else:
column_name = value if value else "unnamed"
# add count to column name
idx = 1
while f"{column_name}.{idx}" in columns:
idx += 1
columns.append(f"{column_name}.{idx}")
elif i == 0:
# without headers, assign generic numbered column names
columns = columns if columns else [f"column.{j}" for j in range(len(row))]
if i == 0: if i == 0:
columns = columns or get_columns(backend, row, headers)
df_dict = OrderedDict((column, []) for column in columns) df_dict = OrderedDict((column, []) for column in columns)
# create index for the column headers # create index for the column headers
col_index = {j: column for j, column in enumerate(columns)} col_index = {j: column for j, column in enumerate(columns)}
if headers: if headers:
continue continue
for j, cell in enumerate(row): for j, cell in enumerate(row):
if j < len(col_index): if j < len(col_index):
value, _ = backend.get_value(cell, parsed=True) value, _ = backend.get_value(cell, parsed=True)
# use header instead of column index # use header instead of column index
df_dict[col_index[j]].append(value) df_dict[col_index[j]].append(value)
# make sure all columns are of the same length # make sure all columns are of the same length
max_col_length = max(len(df_dict[col]) for col in df_dict) max_col_length = max(len(df_dict[col]) for col in df_dict)
for col in df_dict: for col in df_dict:
col_length = len(df_dict[col]) col_length = len(df_dict[col])
if col_length < max_col_length: if col_length < max_col_length:
df_dict[col] += [None] * (max_col_length - col_length) df_dict[col] += [None] * (max_col_length - col_length)
df = pd.DataFrame(df_dict)
return df return pd.DataFrame(df_dict)
def read_data(backend, file_or_path, sheet_id, headers=True, columns=None): def read_data(
backend: ModuleType,
file_or_path: Path,
sheet_id: Union[str, int],
headers: bool,
columns: List[str],
skiprows: int,
) -> pd.DataFrame:
doc = backend.get_doc(file_or_path) doc = backend.get_doc(file_or_path)
rows = backend.get_rows(doc, sheet_id) rows = backend.get_rows(doc, sheet_id)
df = parse_data(backend, rows, headers=headers, columns=columns) df = parse_data(backend, rows, headers=headers, columns=columns, skiprows=skiprows)
return sanitize_df(df) return sanitize_df(df)

View File

@ -1,5 +1,9 @@
"""Imports an ods or fods file into a DataFrame object""" """Imports an ods or fods file into a DataFrame object"""
from pathlib import Path from pathlib import Path
from typing import Optional, List, Union
import pandas as pd
from .parsers import fods, ods from .parsers import fods, ods
from . import algo from . import algo
@ -8,30 +12,38 @@ from . import algo
EXT_MAP = {".ods": ods, ".fods": fods} EXT_MAP = {".ods": ods, ".fods": fods}
def read_ods(file_or_path, sheet=1, headers=True, columns=None): def read_ods(
file_or_path: Union[str, Path],
sheet: Union[str, int] = 1,
headers: bool = True,
columns: Optional[List[str]] = None,
skiprows: int = 0,
) -> pd.DataFrame:
""" """
Read in the provided ods file and convert it to `pandas.DataFrame`. Read in the provided ods or .ods file and convert it to `pandas.DataFrame`.
Will detect the filetype based on the file's extension or fall back to
ods.
Parameters Args:
---------- file_or_path: The path to the .ods or .fods file.
file_or_path : str sheet: If `int`, the 1 based index of the sheet to be read. If `str`, the
The path to the .ods or .fods file. name of the sheet to be read.
sheet : int or str, default 1 headers: If `True`, then the first row is treated as the list of column names.
If `int`, the 1 based index of the sheet to be read. If `str`, the columns: A list of column names to be used as headers.
name of the sheet to be read. skiprows: The number of rows to skip before starting to read data.
header : bool, default True
If `True`, then the first row is treated as the list of column names.
columns : list or None, optional
A list of column names to be used as headers.
Returns Returns:
-------
pandas.DataFrame
The content of the specified sheet as a DataFrame. The content of the specified sheet as a DataFrame.
""" """
backend = EXT_MAP.get(Path(file_or_path).suffix) path = file_or_path if isinstance(file_or_path, Path) else Path(file_or_path)
if not backend: if not path.is_file():
raise ValueError("Unknown filetype.") raise FileNotFoundError(f"file {path} does not exist")
backend = EXT_MAP.get(path.suffix, ods)
return algo.read_data( return algo.read_data(
backend, file_or_path, sheet, headers=headers, columns=columns backend,
path,
sheet,
headers=headers,
columns=columns or [],
skiprows=skiprows,
) )

View File

@ -1,3 +1,6 @@
from pathlib import Path
from typing import Iterator, Optional, Tuple, Union
from lxml import etree from lxml import etree
@ -13,15 +16,16 @@ TABLE_CELL_REPEATED_ATTRIB = "number-columns-repeated"
VALUE_TYPE_ATTRIB = "value-type" VALUE_TYPE_ATTRIB = "value-type"
def get_doc(file_or_path): def get_doc(file_or_path: Path) -> etree._ElementTree:
return etree.parse(str(file_or_path)) return etree.parse(str(file_or_path))
def get_sheet(spreadsheet, sheet_id): def get_sheet(spreadsheet: etree._Element, sheet_id: Union[str, int]) -> etree._Element:
namespaces = spreadsheet.nsmap namespaces = spreadsheet.nsmap
if isinstance(sheet_id, str): if isinstance(sheet_id, str):
sheet = spreadsheet.find( sheet = spreadsheet.find(
f"{TABLE_TAG}[@table:name='{sheet_id}']", namespaces=namespaces f"{TABLE_TAG}[@table:name='{sheet_id}']",
namespaces=namespaces,
) )
if sheet is None: if sheet is None:
raise KeyError(f"There is no sheet named {sheet_id}.") raise KeyError(f"There is no sheet named {sheet_id}.")
@ -32,34 +36,39 @@ def get_sheet(spreadsheet, sheet_id):
return tables[sheet_id - 1] return tables[sheet_id - 1]
def get_rows(doc, sheet_id): def get_rows(
doc: etree._ElementTree,
sheet_id: Union[str, int],
) -> Iterator[etree._Element]:
if not isinstance(sheet_id, (str, int)): if not isinstance(sheet_id, (str, int)):
raise ValueError("Sheet id has to be either `str` or `int`") raise ValueError("Sheet id has to be either `str` or `int`")
root = doc.getroot() root = doc.getroot()
namespaces = root.nsmap namespaces = root.nsmap
spreadsheet = doc.find(BODY_TAG, namespaces=namespaces).find( spreadsheet = doc.find(BODY_TAG, namespaces=namespaces).find( # type: ignore
SPREADSHEET_TAG, namespaces=namespaces SPREADSHEET_TAG, namespaces=namespaces
) )
sheet = get_sheet(spreadsheet, sheet_id) sheet = get_sheet(spreadsheet, sheet_id)
rows = sheet.findall(TABLE_ROW_TAG, namespaces=namespaces) return sheet.iterfind(TABLE_ROW_TAG, namespaces=namespaces)
return rows
def is_float(cell): def is_float(cell: etree._Element) -> bool:
return ( return (
cell.attrib.get(f"{{{cell.nsmap[OFFICE_KEY]}}}{VALUE_TYPE_ATTRIB}") == "float" cell.attrib.get(f"{{{cell.nsmap[OFFICE_KEY]}}}{VALUE_TYPE_ATTRIB}") == "float"
) )
def get_value(cell, parsed=False): def get_value(
cell: etree._Element,
parsed: bool = False,
) -> Tuple[Optional[Union[str, float]], int]:
text = cell.find(TABLE_CELL_TEXT_TAG, namespaces=cell.nsmap) text = cell.find(TABLE_CELL_TEXT_TAG, namespaces=cell.nsmap)
if text is None: if text is None:
return None, 0 return None, 0
value = text.text value: Union[str, float] = "".join(text.itertext())
if parsed and is_float(cell): if parsed and is_float(cell):
value = float(value) value = float(value)
n_repeated = cell.attrib.get( _n_repeated = cell.attrib.get(
f"{{{cell.nsmap[TABLE_KEY]}}}{TABLE_CELL_REPEATED_ATTRIB}" f"{{{cell.nsmap[TABLE_KEY]}}}{TABLE_CELL_REPEATED_ATTRIB}"
) )
n_repeated = int(n_repeated) if n_repeated is not None else 0 n_repeated = int(_n_repeated) if _n_repeated is not None else 0
return value, n_repeated return value, n_repeated

View File

@ -1,21 +1,28 @@
import ezodf from pathlib import Path
from typing import Any, Iterator, List, Tuple, Union
import ezodf # type: ignore[import]
from ezodf.document import FlatXMLDocument, PackagedDocument # type: ignore[import]
def get_doc(file_or_path): def get_doc(file_or_path: Path) -> Union[FlatXMLDocument, PackagedDocument]:
return ezodf.opendoc(file_or_path) return ezodf.opendoc(file_or_path)
def get_rows(doc, sheet_id): def get_rows(
doc: Union[FlatXMLDocument, PackagedDocument],
sheet_id: Union[str, int],
) -> Iterator[List[ezodf.Cell]]:
if not isinstance(sheet_id, (int, str)): if not isinstance(sheet_id, (int, str)):
raise ValueError("Sheet id has to be either `str` or `int`") raise ValueError("Sheet id has to be either `str` or `int`")
if isinstance(sheet_id, str): if isinstance(sheet_id, str):
sheets = [sheet.name for sheet in doc.sheets] sheets: List[str] = [sheet.name for sheet in doc.sheets]
if sheet_id not in sheets: if sheet_id not in sheets:
raise KeyError("There is no sheet named {}".format(sheet_id)) raise KeyError("There is no sheet named {}".format(sheet_id))
sheet_id = sheets.index(sheet_id) + 1 sheet_id = sheets.index(sheet_id) + 1
sheet = doc.sheets[sheet_id - 1] sheet: ezodf.Sheet = doc.sheets[sheet_id - 1]
return sheet.rows() return sheet.rows()
def get_value(cell, parsed=False): def get_value(cell: ezodf.Cell, parsed: bool = False) -> Tuple[Any, int]:
return cell.value, 0 return cell.value, 0

View File

View File

@ -1,5 +1,7 @@
"""Provides utility functions for the parser""" """Provides utility functions for the parser"""
import pandas as pd
def ods_info(doc): def ods_info(doc):
"""Print the number of sheets, their names, and number of rows and columns""" """Print the number of sheets, their names, and number of rows and columns"""
@ -14,8 +16,8 @@ def ods_info(doc):
) )
def sanitize_df(df): def sanitize_df(df: pd.DataFrame) -> pd.DataFrame:
"""Drop empty rows and columns from the DataFrame and returns it""" """Drop empty rows and columns from the DataFrame and return it."""
# Delete empty rows # Delete empty rows
for i in df.index.tolist()[-1::-1]: for i in df.index.tolist()[-1::-1]:
if df.iloc[i].isna().all(): if df.iloc[i].isna().all():

1909
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

42
pyproject.toml Normal file
View File

@ -0,0 +1,42 @@
[tool.poetry]
name = "pandas-ods-reader"
version = "1.0.2"
description = "Read in .ods and .fods files and return a pandas.DataFrame."
authors = ["iuvbio <iuvbio@users.noreply.github.com>"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/iuvbio/pandas_ods_reader"
keywords = ["data", "io", "pandas", "ods"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Topic :: Utilities",
]
[tool.poetry.dependencies]
python = ">=3.9,<4"
ezodf = ">=0.3.2"
lxml = ">=4.9.2"
pandas = ">=2.2.3"
[tool.poetry.group.dev.dependencies]
black = ">=22.10.0"
pytest = ">=7.1.3"
pytest-cov = ">=4.0.0"
mypy = ">=0.991"
flake8 = ">=6.0.0"
pandas-stubs = ">=1.5.2.221213"
types-lxml = ">=2022.11.8"
commitizen = ">=2.38.0"
pre-commit = ">=3.7.1"
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "v$version"
version_provider = "poetry"
version_files = ["pyproject.toml:version"]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@ -1,31 +0,0 @@
[metadata]
name = pandas-ods-reader
version = file: pandas_ods_reader/VERSION
description = Read in an ODS file and return it as a pandas.DataFrame
long_description = file: README.md, LICENSE.txt
long_description_content_type = text/markdown
classifiers =
Development Status :: 5 - Production/Stable
License :: OSI Approved :: MIT License
Programming Language :: Python :: 3
Topic :: Utilities
keywords = data, io, pandas, ods
url = https://github.com/iuvbio/pandas_ods_reader
author = iuvbio
author_email = cryptodemigod@protonmail.com
license = MIT
[options]
zip_safe = False
packages = find:
include_package_data = True
install_requires =
ezodf
lxml
pandas
[options.extras_require]
test = pytest
[aliases]
test = pytest

View File

@ -1,7 +0,0 @@
from setuptools import setup
if __name__ == "__main__":
setup(
# see setup.cfg
)

View File

@ -0,0 +1,423 @@
<?xml version="1.0" encoding="UTF-8"?>
<office:document xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" xmlns:css3t="http://www.w3.org/TR/css3-text/" xmlns:grddl="http://www.w3.org/2003/g/data-view#" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:rpt="http://openoffice.org/2005/report" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:ooo="http://openoffice.org/2004/office" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:drawooo="http://openoffice.org/2010/draw" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:tableooo="http://openoffice.org/2009/table" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:xforms="http://www.w3.org/2002/xforms" office:version="1.2" office:mimetype="application/vnd.oasis.opendocument.spreadsheet">
<office:meta><meta:initial-creator>Lukas Jansen</meta:initial-creator><meta:creation-date>2019-01-27T03:31:08.931482632</meta:creation-date><dc:date>2022-10-25T08:45:33.990049580</dc:date><meta:editing-duration>PT2M33S</meta:editing-duration><meta:editing-cycles>2</meta:editing-cycles><meta:generator>LibreOffice/6.4.7.2$Linux_X86_64 LibreOffice_project/40$Build-2</meta:generator><meta:document-statistic meta:table-count="1" meta:cell-count="55" meta:object-count="0"/></office:meta>
<office:settings>
<config:config-item-set config:name="ooo:view-settings">
<config:config-item config:name="VisibleAreaTop" config:type="int">0</config:config-item>
<config:config-item config:name="VisibleAreaLeft" config:type="int">0</config:config-item>
<config:config-item config:name="VisibleAreaWidth" config:type="int">11288</config:config-item>
<config:config-item config:name="VisibleAreaHeight" config:type="int">4967</config:config-item>
<config:config-item-map-indexed config:name="Views">
<config:config-item-map-entry>
<config:config-item config:name="ViewId" config:type="string">view1</config:config-item>
<config:config-item-map-named config:name="Tables">
<config:config-item-map-entry config:name="Sheet1">
<config:config-item config:name="CursorPositionX" config:type="int">5</config:config-item>
<config:config-item config:name="CursorPositionY" config:type="int">2</config:config-item>
<config:config-item config:name="HorizontalSplitMode" config:type="short">0</config:config-item>
<config:config-item config:name="VerticalSplitMode" config:type="short">0</config:config-item>
<config:config-item config:name="HorizontalSplitPosition" config:type="int">0</config:config-item>
<config:config-item config:name="VerticalSplitPosition" config:type="int">0</config:config-item>
<config:config-item config:name="ActiveSplitRange" config:type="short">2</config:config-item>
<config:config-item config:name="PositionLeft" config:type="int">0</config:config-item>
<config:config-item config:name="PositionRight" config:type="int">0</config:config-item>
<config:config-item config:name="PositionTop" config:type="int">0</config:config-item>
<config:config-item config:name="PositionBottom" config:type="int">0</config:config-item>
<config:config-item config:name="ZoomType" config:type="short">0</config:config-item>
<config:config-item config:name="ZoomValue" config:type="int">100</config:config-item>
<config:config-item config:name="PageViewZoomValue" config:type="int">60</config:config-item>
<config:config-item config:name="ShowGrid" config:type="boolean">true</config:config-item>
<config:config-item config:name="AnchoredTextOverflowLegacy" config:type="boolean">false</config:config-item>
</config:config-item-map-entry>
</config:config-item-map-named>
<config:config-item config:name="ActiveTable" config:type="string">Sheet1</config:config-item>
<config:config-item config:name="HorizontalScrollbarWidth" config:type="int">1861</config:config-item>
<config:config-item config:name="ZoomType" config:type="short">0</config:config-item>
<config:config-item config:name="ZoomValue" config:type="int">100</config:config-item>
<config:config-item config:name="PageViewZoomValue" config:type="int">60</config:config-item>
<config:config-item config:name="ShowPageBreakPreview" config:type="boolean">false</config:config-item>
<config:config-item config:name="ShowZeroValues" config:type="boolean">true</config:config-item>
<config:config-item config:name="ShowNotes" config:type="boolean">true</config:config-item>
<config:config-item config:name="ShowGrid" config:type="boolean">true</config:config-item>
<config:config-item config:name="GridColor" config:type="int">12632256</config:config-item>
<config:config-item config:name="ShowPageBreaks" config:type="boolean">true</config:config-item>
<config:config-item config:name="HasColumnRowHeaders" config:type="boolean">true</config:config-item>
<config:config-item config:name="HasSheetTabs" config:type="boolean">true</config:config-item>
<config:config-item config:name="IsOutlineSymbolsSet" config:type="boolean">true</config:config-item>
<config:config-item config:name="IsValueHighlightingEnabled" config:type="boolean">false</config:config-item>
<config:config-item config:name="IsSnapToRaster" config:type="boolean">false</config:config-item>
<config:config-item config:name="RasterIsVisible" config:type="boolean">false</config:config-item>
<config:config-item config:name="RasterResolutionX" config:type="int">1000</config:config-item>
<config:config-item config:name="RasterResolutionY" config:type="int">1000</config:config-item>
<config:config-item config:name="RasterSubdivisionX" config:type="int">1</config:config-item>
<config:config-item config:name="RasterSubdivisionY" config:type="int">1</config:config-item>
<config:config-item config:name="IsRasterAxisSynchronized" config:type="boolean">true</config:config-item>
<config:config-item config:name="AnchoredTextOverflowLegacy" config:type="boolean">false</config:config-item>
</config:config-item-map-entry>
</config:config-item-map-indexed>
</config:config-item-set>
<config:config-item-set config:name="ooo:configuration-settings">
<config:config-item config:name="EmbedComplexScriptFonts" config:type="boolean">true</config:config-item>
<config:config-item config:name="EmbedAsianScriptFonts" config:type="boolean">true</config:config-item>
<config:config-item config:name="EmbedLatinScriptFonts" config:type="boolean">true</config:config-item>
<config:config-item config:name="EmbedOnlyUsedFonts" config:type="boolean">false</config:config-item>
<config:config-item config:name="RasterResolutionY" config:type="int">1000</config:config-item>
<config:config-item config:name="IsOutlineSymbolsSet" config:type="boolean">true</config:config-item>
<config:config-item config:name="RasterSubdivisionY" config:type="int">1</config:config-item>
<config:config-item config:name="GridColor" config:type="int">12632256</config:config-item>
<config:config-item config:name="HasColumnRowHeaders" config:type="boolean">true</config:config-item>
<config:config-item config:name="ShowNotes" config:type="boolean">true</config:config-item>
<config:config-item config:name="HasSheetTabs" config:type="boolean">true</config:config-item>
<config:config-item config:name="PrinterSetup" config:type="base64Binary">kwH+/0dlbmVyaWMgUHJpbnRlcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU0dFTlBSVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAMAtAAAAAAAAAAEAAhSAAAEdAAASm9iRGF0YSAxCnByaW50ZXI9R2VuZXJpYyBQcmludGVyCm9yaWVudGF0aW9uPVBvcnRyYWl0CmNvcGllcz0xCmNvbGxhdGU9ZmFsc2UKbWFyZ2luZGFqdXN0bWVudD0wLDAsMCwwCmNvbG9yZGVwdGg9MjQKcHNsZXZlbD0wCnBkZmRldmljZT0xCmNvbG9yZGV2aWNlPTAKUFBEQ29udGV4RGF0YQpQYWdlU2l6ZTpBNAAAEgBDT01QQVRfRFVQTEVYX01PREUPAER1cGxleE1vZGU6Ok9mZg==</config:config-item>
<config:config-item config:name="RasterResolutionX" config:type="int">1000</config:config-item>
<config:config-item config:name="SyntaxStringRef" config:type="short">7</config:config-item>
<config:config-item config:name="RasterIsVisible" config:type="boolean">false</config:config-item>
<config:config-item config:name="ShowZeroValues" config:type="boolean">true</config:config-item>
<config:config-item config:name="ApplyUserData" config:type="boolean">true</config:config-item>
<config:config-item config:name="RasterSubdivisionX" config:type="int">1</config:config-item>
<config:config-item config:name="IsRasterAxisSynchronized" config:type="boolean">true</config:config-item>
<config:config-item config:name="LoadReadonly" config:type="boolean">false</config:config-item>
<config:config-item config:name="AutoCalculate" config:type="boolean">true</config:config-item>
<config:config-item config:name="EmbedFonts" config:type="boolean">false</config:config-item>
<config:config-item config:name="SaveThumbnail" config:type="boolean">true</config:config-item>
<config:config-item config:name="ShowGrid" config:type="boolean">true</config:config-item>
<config:config-item config:name="PrinterName" config:type="string">Generic Printer</config:config-item>
<config:config-item config:name="PrinterPaperFromSetup" config:type="boolean">false</config:config-item>
<config:config-item config:name="CharacterCompressionType" config:type="short">0</config:config-item>
<config:config-item config:name="LinkUpdateMode" config:type="short">3</config:config-item>
<config:config-item config:name="ShowPageBreaks" config:type="boolean">true</config:config-item>
<config:config-item config:name="SaveVersionOnClose" config:type="boolean">false</config:config-item>
<config:config-item config:name="IsSnapToRaster" config:type="boolean">false</config:config-item>
<config:config-item config:name="IsKernAsianPunctuation" config:type="boolean">false</config:config-item>
<config:config-item config:name="UpdateFromTemplate" config:type="boolean">true</config:config-item>
<config:config-item config:name="IsDocumentShared" config:type="boolean">false</config:config-item>
<config:config-item config:name="AllowPrintJobCancel" config:type="boolean">true</config:config-item>
</config:config-item-set>
</office:settings>
<office:scripts>
<office:script script:language="ooo:Basic">
<ooo:libraries xmlns:ooo="http://openoffice.org/2004/office" xmlns:xlink="http://www.w3.org/1999/xlink"/>
</office:script>
</office:scripts>
<office:font-face-decls>
<style:font-face style:name="Liberation Sans" svg:font-family="&apos;Liberation Sans&apos;" style:font-family-generic="swiss" style:font-pitch="variable"/>
<style:font-face style:name="Lohit Devanagari" svg:font-family="&apos;Lohit Devanagari&apos;" style:font-family-generic="system" style:font-pitch="variable"/>
<style:font-face style:name="Noto Sans CJK SC" svg:font-family="&apos;Noto Sans CJK SC&apos;" style:font-family-generic="system" style:font-pitch="variable"/>
<style:font-face style:name="Noto Sans CJK SC Regular" svg:font-family="&apos;Noto Sans CJK SC Regular&apos;" style:font-family-generic="system" style:font-pitch="variable"/>
</office:font-face-decls>
<office:styles>
<style:default-style style:family="table-cell">
<style:paragraph-properties style:tab-stop-distance="1.25cm"/>
<style:text-properties style:font-name="Liberation Sans" fo:language="en" fo:country="NZ" style:font-name-asian="Noto Sans CJK SC Regular" style:language-asian="zh" style:country-asian="CN" style:font-name-complex="Lohit Devanagari" style:language-complex="hi" style:country-complex="IN"/>
</style:default-style>
<number:number-style style:name="N0">
<number:number number:min-integer-digits="1"/>
</number:number-style>
<style:style style:name="Default" style:family="table-cell"/>
<style:style style:name="Heading" style:family="table-cell" style:parent-style-name="Default">
<style:text-properties fo:color="#000000" fo:font-size="24pt" fo:font-style="normal" fo:font-weight="bold"/>
</style:style>
<style:style style:name="Heading_20_1" style:display-name="Heading 1" style:family="table-cell" style:parent-style-name="Heading">
<style:text-properties fo:color="#000000" fo:font-size="18pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Heading_20_2" style:display-name="Heading 2" style:family="table-cell" style:parent-style-name="Heading">
<style:text-properties fo:color="#000000" fo:font-size="12pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Text" style:family="table-cell" style:parent-style-name="Default"/>
<style:style style:name="Note" style:family="table-cell" style:parent-style-name="Text">
<style:table-cell-properties fo:background-color="#ffffcc" style:diagonal-bl-tr="none" style:diagonal-tl-br="none" fo:border="0.74pt solid #808080"/>
<style:text-properties fo:color="#333333" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Footnote" style:family="table-cell" style:parent-style-name="Text">
<style:text-properties fo:color="#808080" fo:font-size="10pt" fo:font-style="italic" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Hyperlink" style:family="table-cell" style:parent-style-name="Text">
<style:text-properties fo:color="#0000ee" fo:font-size="10pt" fo:font-style="normal" style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="#0000ee" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Status" style:family="table-cell" style:parent-style-name="Default"/>
<style:style style:name="Good" style:family="table-cell" style:parent-style-name="Status">
<style:table-cell-properties fo:background-color="#ccffcc"/>
<style:text-properties fo:color="#006600" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Neutral" style:family="table-cell" style:parent-style-name="Status">
<style:table-cell-properties fo:background-color="#ffffcc"/>
<style:text-properties fo:color="#996600" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Bad" style:family="table-cell" style:parent-style-name="Status">
<style:table-cell-properties fo:background-color="#ffcccc"/>
<style:text-properties fo:color="#cc0000" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Warning" style:family="table-cell" style:parent-style-name="Status">
<style:text-properties fo:color="#cc0000" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Error" style:family="table-cell" style:parent-style-name="Status">
<style:table-cell-properties fo:background-color="#cc0000"/>
<style:text-properties fo:color="#ffffff" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="bold"/>
</style:style>
<style:style style:name="Accent" style:family="table-cell" style:parent-style-name="Default">
<style:text-properties fo:color="#000000" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="bold"/>
</style:style>
<style:style style:name="Accent_20_1" style:display-name="Accent 1" style:family="table-cell" style:parent-style-name="Accent">
<style:table-cell-properties fo:background-color="#000000"/>
<style:text-properties fo:color="#ffffff" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Accent_20_2" style:display-name="Accent 2" style:family="table-cell" style:parent-style-name="Accent">
<style:table-cell-properties fo:background-color="#808080"/>
<style:text-properties fo:color="#ffffff" fo:font-size="10pt" fo:font-style="normal" fo:font-weight="normal"/>
</style:style>
<style:style style:name="Accent_20_3" style:display-name="Accent 3" style:family="table-cell" style:parent-style-name="Accent">
<style:table-cell-properties fo:background-color="#dddddd"/>
</style:style>
<style:style style:name="Result" style:family="table-cell" style:parent-style-name="Default">
<style:text-properties fo:color="#000000" fo:font-size="10pt" fo:font-style="italic" style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="#000000" fo:font-weight="bold"/>
</style:style>
</office:styles>
<office:automatic-styles>
<style:style style:name="co1" style:family="table-column">
<style:table-column-properties fo:break-before="auto" style:column-width="2.258cm"/>
</style:style>
<style:style style:name="ro1" style:family="table-row">
<style:table-row-properties style:row-height="0.452cm" fo:break-before="auto" style:use-optimal-row-height="true"/>
</style:style>
<style:style style:name="ta1" style:family="table" style:master-page-name="Default">
<style:table-properties table:display="true" style:writing-mode="lr-tb"/>
</style:style>
<number:number-style style:name="N2">
<number:number number:decimal-places="2" loext:min-decimal-places="2" number:min-integer-digits="1"/>
</number:number-style>
<style:page-layout style:name="pm1">
<style:page-layout-properties style:writing-mode="lr-tb"/>
<style:header-style>
<style:header-footer-properties fo:min-height="0.75cm" fo:margin-left="0cm" fo:margin-right="0cm" fo:margin-bottom="0.25cm"/>
</style:header-style>
<style:footer-style>
<style:header-footer-properties fo:min-height="0.75cm" fo:margin-left="0cm" fo:margin-right="0cm" fo:margin-top="0.25cm"/>
</style:footer-style>
</style:page-layout>
<style:page-layout style:name="pm2">
<style:page-layout-properties style:writing-mode="lr-tb"/>
<style:header-style>
<style:header-footer-properties fo:min-height="0.75cm" fo:margin-left="0cm" fo:margin-right="0cm" fo:margin-bottom="0.25cm" fo:border="2.49pt solid #000000" fo:padding="0.018cm" fo:background-color="#c0c0c0">
<style:background-image/>
</style:header-footer-properties>
</style:header-style>
<style:footer-style>
<style:header-footer-properties fo:min-height="0.75cm" fo:margin-left="0cm" fo:margin-right="0cm" fo:margin-top="0.25cm" fo:border="2.49pt solid #000000" fo:padding="0.018cm" fo:background-color="#c0c0c0">
<style:background-image/>
</style:header-footer-properties>
</style:footer-style>
</style:page-layout>
</office:automatic-styles>
<office:master-styles>
<style:master-page style:name="Default" style:page-layout-name="pm1">
<style:header>
<text:p><text:sheet-name>???</text:sheet-name></text:p>
</style:header>
<style:header-left style:display="false"/>
<style:footer>
<text:p>Page <text:page-number>1</text:page-number></text:p>
</style:footer>
<style:footer-left style:display="false"/>
</style:master-page>
<style:master-page style:name="Report" style:page-layout-name="pm2">
<style:header>
<style:region-left>
<text:p><text:sheet-name>???</text:sheet-name><text:s/>(<text:title>???</text:title>)</text:p>
</style:region-left>
<style:region-right>
<text:p><text:date style:data-style-name="N2" text:date-value="2022-10-25">00/00/0000</text:date>, <text:time style:data-style-name="N2" text:time-value="08:45:14.557684313">00:00:00</text:time></text:p>
</style:region-right>
</style:header>
<style:header-left style:display="false"/>
<style:footer>
<text:p>Page <text:page-number>1</text:page-number><text:s/>/ <text:page-count>99</text:page-count></text:p>
</style:footer>
<style:footer-left style:display="false"/>
</style:master-page>
</office:master-styles>
<office:body>
<office:spreadsheet>
<table:calculation-settings table:automatic-find-labels="false" table:use-regular-expressions="false" table:use-wildcards="true"/>
<table:table table:name="Sheet1" table:style-name="ta1">
<table:table-column table:style-name="co1" table:number-columns-repeated="5" table:default-cell-style-name="Default"/>
<table:table-row table:style-name="ro1">
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>A</text:p>
</table:table-cell>
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>B</text:p>
</table:table-cell>
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>C</text:p>
</table:table-cell>
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>D</text:p>
</table:table-cell>
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>E</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell table:number-columns-repeated="5" office:value-type="string" calcext:value-type="string">
<text:p>skip this</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>a</text:p>
</table:table-cell>
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>b</text:p>
</table:table-cell>
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>c</text:p>
</table:table-cell>
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>d</text:p>
</table:table-cell>
<table:table-cell office:value-type="string" calcext:value-type="string">
<text:p>e</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="10" calcext:value-type="float">
<text:p>10</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="0" calcext:value-type="float">
<text:p>0</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="58" calcext:value-type="float">
<text:p>58</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="29" calcext:value-type="float">
<text:p>29</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="77" calcext:value-type="float">
<text:p>77</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="5" calcext:value-type="float">
<text:p>5</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="47" calcext:value-type="float">
<text:p>47</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="50" calcext:value-type="float">
<text:p>50</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="99" calcext:value-type="float">
<text:p>99</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="79" calcext:value-type="float">
<text:p>79</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="75" calcext:value-type="float">
<text:p>75</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="25" calcext:value-type="float">
<text:p>25</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="86" calcext:value-type="float">
<text:p>86</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="47" calcext:value-type="float">
<text:p>47</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="65" calcext:value-type="float">
<text:p>65</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="82" calcext:value-type="float">
<text:p>82</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="45" calcext:value-type="float">
<text:p>45</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="88" calcext:value-type="float">
<text:p>88</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="48" calcext:value-type="float">
<text:p>48</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="74" calcext:value-type="float">
<text:p>74</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="72" calcext:value-type="float">
<text:p>72</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="47" calcext:value-type="float">
<text:p>47</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="57" calcext:value-type="float">
<text:p>57</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="82" calcext:value-type="float">
<text:p>82</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="46" calcext:value-type="float">
<text:p>46</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="40" calcext:value-type="float">
<text:p>40</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="54" calcext:value-type="float">
<text:p>54</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="26" calcext:value-type="float">
<text:p>26</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="97" calcext:value-type="float">
<text:p>97</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="1" calcext:value-type="float">
<text:p>1</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="54" calcext:value-type="float">
<text:p>54</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="26" calcext:value-type="float">
<text:p>26</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="99" calcext:value-type="float">
<text:p>99</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="63" calcext:value-type="float">
<text:p>63</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="49" calcext:value-type="float">
<text:p>49</text:p>
</table:table-cell>
</table:table-row>
<table:table-row table:style-name="ro1">
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="87" calcext:value-type="float">
<text:p>87</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="24" calcext:value-type="float">
<text:p>24</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="47" calcext:value-type="float">
<text:p>47</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="87" calcext:value-type="float">
<text:p>87</text:p>
</table:table-cell>
<table:table-cell table:formula="of:=RANDBETWEEN(0; 100)" office:value-type="float" office:value="15" calcext:value-type="float">
<text:p>15</text:p>
</table:table-cell>
</table:table-row>
</table:table>
<table:named-expressions/>
</office:spreadsheet>
</office:body>
</office:document>

Binary file not shown.

View File

@ -1,4 +1,5 @@
"""Tests for core read_ods function with different files""" """Tests for core read_ods function with different files"""
from pathlib import Path from pathlib import Path
import pandas as pd import pandas as pd
@ -16,132 +17,161 @@ duplicated_column_names_file = "example_duplicated_column_names.ods"
col_len_file = "example_col_lengths.ods" col_len_file = "example_col_lengths.ods"
missing_header_file = "example_missing_header.ods" missing_header_file = "example_missing_header.ods"
mixed_dtypes_file = "mixed_dtypes.ods" mixed_dtypes_file = "mixed_dtypes.ods"
skiprows_file = "example_skiprows.ods"
class TestOdsReader: @pytest.mark.parametrize("suffix", [".ods", ".fods"])
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) def test_header_file_simple(suffix: str) -> None:
def test_header_file_simple(self, suffix): """Test a simple file with headers."""
path = rsc / header_file
df = read_ods(path.with_suffix(suffix))
path = rsc / header_file assert isinstance(df, pd.DataFrame)
df = read_ods(path.with_suffix(suffix)) assert len(df) == 10
assert len(df.columns) == 5
assert isinstance(df, pd.DataFrame)
assert len(df) == 10
assert len(df.columns) == 5
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_header_file_with_int(self, suffix): def test_header_file_with_int(suffix: str) -> None:
"""Test referencing a sheet by index."""
path = rsc / header_file
df = read_ods(path.with_suffix(suffix), 1)
path = rsc / header_file assert isinstance(df, pd.DataFrame)
df = read_ods(path.with_suffix(suffix), 1) assert len(df) == 10
assert len(df.columns) == 5
assert isinstance(df, pd.DataFrame)
assert len(df) == 10
assert len(df.columns) == 5
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_header_file_with_str(self, suffix): def test_header_file_with_str(suffix: str) -> None:
"""Test referencing a sheet by name."""
path = rsc / header_file
df = read_ods(path.with_suffix(suffix), "Sheet1")
path = rsc / header_file assert isinstance(df, pd.DataFrame)
df = read_ods(path.with_suffix(suffix), "Sheet1") assert len(df) == 10
assert len(df.columns) == 5
assert isinstance(df, pd.DataFrame)
assert len(df) == 10
assert len(df.columns) == 5
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_header_file_with_cols(self, suffix): def test_header_file_with_cols(suffix: str) -> None:
"""Test overwriting haders with column names."""
path = rsc / header_file
columns = ["One", "Two", "Three", "Four", "Five"]
df = read_ods(path.with_suffix(suffix), "Sheet1", columns=columns)
path = rsc / header_file assert list(df.columns) == columns
columns = ["One", "Two", "Three", "Four", "Five"] assert len(df) == 10
df = read_ods(path.with_suffix(suffix), "Sheet1", columns=columns) assert len(df.columns) == 5
assert list(df.columns) == columns
assert len(df) == 10
assert len(df.columns) == 5
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_no_header_file_no_cols(self, suffix): def test_no_header_file_no_cols(suffix: str) -> None:
"""Test autogeneration of headers with no headers and not column names."""
path = rsc / no_header_file
df = read_ods(path.with_suffix(suffix), 1, headers=False)
path = rsc / no_header_file assert list(df.columns) == [f"column.{i}" for i in range(len(df.columns))]
df = read_ods(path.with_suffix(suffix), 1, headers=False) assert len(df) == 10
assert len(df.columns) == 5
assert list(df.columns) == [f"column.{i}" for i in range(len(df.columns))]
assert len(df) == 10
assert len(df.columns) == 5
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_no_header_file_with_cols(self, suffix): def test_no_header_file_with_cols(suffix: str) -> None:
"""Test reading a file with no headers and passing column names."""
path = rsc / no_header_file
columns = ["A", "B", "C", "D", "E"]
df = read_ods(path.with_suffix(suffix), 1, headers=False, columns=columns)
path = rsc / no_header_file assert list(df.columns) == columns
columns = ["A", "B", "C", "D", "E"] assert len(df) == 10
df = read_ods(path.with_suffix(suffix), 1, headers=False, columns=columns)
assert list(df.columns) == columns
assert len(df) == 10
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_duplicated_column_names(self, suffix): def test_duplicated_column_names(suffix: str) -> None:
"""Test numbering duplicate column names."""
path = rsc / duplicated_column_names_file
df = read_ods(path.with_suffix(suffix), 1)
path = rsc / duplicated_column_names_file assert isinstance(df, pd.DataFrame)
df = read_ods(path.with_suffix(suffix), 1) assert len(df.columns) == 4
assert "website.1" in df.columns
assert isinstance(df, pd.DataFrame)
assert len(df.columns) == 4
assert "website.1" in df.columns
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_header_file_col_len(self, suffix): def test_header_file_col_len(suffix: str) -> None:
"""Test the correct number of columns is read."""
path = rsc / col_len_file
df = read_ods(path.with_suffix(suffix), 1)
path = rsc / col_len_file assert isinstance(df, pd.DataFrame)
df = read_ods(path.with_suffix(suffix), 1) assert len(df) == 10
assert len(df.columns) == 5
assert isinstance(df, pd.DataFrame)
assert len(df) == 10
assert len(df.columns) == 5
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_wrong_id_type(self, suffix): def test_wrong_id_type(suffix: str) -> None:
"""Verify passing a wrong id type raises an error."""
path = rsc / header_file
path = rsc / header_file with pytest.raises(ValueError) as e_info:
read_ods(path.with_suffix(suffix), 1.0) # type: ignore[arg-type]
assert e_info.match("Sheet id has to be either `str` or `int`")
with pytest.raises(ValueError) as e_info:
read_ods(path.with_suffix(suffix), 1.0)
assert e_info.match("Sheet id has to be either `str` or `int`")
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_non_existent_sheet(self, suffix): def test_non_existent_sheet(suffix: str) -> None:
"""Verify referencing a non-existent sheet raises an error."""
path = rsc / header_file
sheet_name = "No_Sheet"
path = rsc / header_file with pytest.raises(KeyError) as e_info:
sheet_name = "No_Sheet" read_ods(path.with_suffix(suffix), sheet_name)
assert e_info.match(f"There is no sheet named {sheet_name}")
with pytest.raises(KeyError) as e_info:
read_ods(path.with_suffix(suffix), sheet_name)
assert e_info.match(f"There is no sheet named {sheet_name}")
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_missing_header(self, suffix): def test_missing_header(suffix: str) -> None:
"""Verify that a missing header is named."""
path = rsc / missing_header_file
df = read_ods(path.with_suffix(suffix), 1)
path = rsc / missing_header_file assert isinstance(df, pd.DataFrame)
df = read_ods(path.with_suffix(suffix), 1) assert len(df) == 10
assert len(df.columns) == 5
assert isinstance(df, pd.DataFrame) assert df.columns[2] == "unnamed.1"
assert len(df) == 10
assert len(df.columns) == 5
assert df.columns[2] == "unnamed.1"
@pytest.mark.parametrize("suffix", [".ods", ".fods"]) @pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_mixed_dtypes(self, suffix): def test_mixed_dtypes(suffix: str) -> None:
"""Verify loading a df with mixed types."""
path = rsc / mixed_dtypes_file
df = read_ods(path.with_suffix(suffix), 1)
path = rsc / mixed_dtypes_file assert isinstance(df, pd.DataFrame)
df = read_ods(path.with_suffix(suffix), 1) assert len(df) == 10
assert len(df.columns) == 5
assert isinstance(df, pd.DataFrame) type_list = [float, object, float, float, object]
assert len(df) == 10 assert df.dtypes.tolist() == type_list
assert len(df.columns) == 5 col_b_types = [type(v) for v in df.B.values]
assert str in col_b_types and float in col_b_types
type_list = [float, object, float, float, object]
assert df.dtypes.tolist() == type_list @pytest.mark.parametrize("suffix", [".ods", ".fods"])
col_b_types = [type(v) for v in df.B.values] def test_skiprows(suffix: str) -> None:
assert str in col_b_types and float in col_b_types """Verify skipping rows works correctly."""
path = rsc / skiprows_file
df = read_ods(path.with_suffix(suffix), skiprows=2)
assert isinstance(df, pd.DataFrame)
assert len(df) == 8
assert len(df.columns) == 5
assert df.columns.tolist() == ["a", "b", "c", "d", "e"]
def test_invalid_path() -> None:
"""Verify passing an invalid path raises an error."""
path = rsc / "does-not-exist.ods"
with pytest.raises(FileNotFoundError, match="does not exist"):
read_ods(path)