Compare commits

..

100 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
iuvbio 6ed1140e11 version 0.1.2 2021-08-22 19:57:43 +02:00
iuvbio 7e270b3b95 importlib.metadata is only built-in since python 3.8 2021-08-22 19:52:07 +02:00
iuvbio 434982c7a9 change underscores 2021-08-22 19:51:31 +02:00
20 changed files with 2715 additions and 230 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:
push:
branches: [ master ]
branches: ["**"]
pull_request:
branches: [ master ]
branches: [master]
jobs:
build:
test:
name: Test on ${{ matrix.python_version }}
runs-on: ubuntu-latest
strategy:
matrix:
python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
fail-fast: false
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install black pytest
pip install .
- name: Lint with black
run: |
# stop the build if there are Python syntax errors or undefined names
black --check --diff pandas_ods_reader/ tests/
- name: Test with pytest
run: |
python -m pytest tests/
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python_version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python_version }}
- name: Install poetry
uses: abatilo/actions-poetry@v4
with:
poetry-version: "2.1.2"
- name: Configure poetry
run: |
poetry config virtualenvs.create true --local
poetry config virtualenvs.in-project true --local
- uses: actions/cache@v4
name: Define a cache for the virtual environment based on the dependencies lock file
with:
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/
# 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.1

View File

@ -1,6 +1,11 @@
import importlib.metadata
import sys
from .main import read_ods
if sys.version_info >= (3, 8):
from importlib.metadata import version
else:
from importlib_metadata import version
__version__ = importlib.metadata.version("pandas-ods-reader")
__version__ = version("pandas-ods-reader")

View File

@ -1,66 +1,99 @@
from collections import OrderedDict
from pathlib import Path
from types import ModuleType
from typing import Any, Dict, Iterator, List, Union
import pandas as pd
from .utils import sanitize_df
def parse_data(backend, rows, headers=True, columns=None):
df_dict = OrderedDict()
col_index = {}
def get_columns_from_headers(backend: ModuleType, row: Any) -> List[str]:
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}")
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):
# 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:
columns = columns or get_columns(backend, row, headers)
df_dict = OrderedDict((column, []) for column in columns)
# create index for the column headers
col_index = {j: column for j, column in enumerate(columns)}
if headers:
continue
for j, cell in enumerate(row):
if j < len(col_index):
value, _ = backend.get_value(cell, parsed=True)
# use header instead of column index
df_dict[col_index[j]].append(value)
# make sure all columns are of the same length
max_col_length = max(len(df_dict[col]) for col in df_dict)
for col in df_dict:
col_length = len(df_dict[col])
if col_length < max_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)
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)

View File

@ -1,5 +1,9 @@
"""Imports an ods or fods file into a DataFrame object"""
from pathlib import Path
from typing import Optional, List, Union
import pandas as pd
from .parsers import fods, ods
from . import algo
@ -8,30 +12,38 @@ from . import algo
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
----------
file_or_path : str
The path to the .ods or .fods file.
sheet : int or str, default 1
If `int`, the 1 based index of the sheet to be read. If `str`, the
name of the sheet to be read.
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.
Args:
file_or_path: The path to the .ods or .fods file.
sheet: If `int`, the 1 based index of the sheet to be read. If `str`, the
name of the sheet to be read.
headers: If `True`, then the first row is treated as the list of column names.
columns: A list of column names to be used as headers.
skiprows: The number of rows to skip before starting to read data.
Returns
-------
pandas.DataFrame
Returns:
The content of the specified sheet as a DataFrame.
"""
backend = EXT_MAP.get(Path(file_or_path).suffix)
if not backend:
raise ValueError("Unknown filetype.")
path = file_or_path if isinstance(file_or_path, Path) else Path(file_or_path)
if not path.is_file():
raise FileNotFoundError(f"file {path} does not exist")
backend = EXT_MAP.get(path.suffix, ods)
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
@ -13,15 +16,16 @@ TABLE_CELL_REPEATED_ATTRIB = "number-columns-repeated"
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))
def get_sheet(spreadsheet, sheet_id):
def get_sheet(spreadsheet: etree._Element, sheet_id: Union[str, int]) -> etree._Element:
namespaces = spreadsheet.nsmap
if isinstance(sheet_id, str):
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:
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]
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)):
raise ValueError("Sheet id has to be either `str` or `int`")
root = doc.getroot()
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
)
sheet = get_sheet(spreadsheet, sheet_id)
rows = sheet.findall(TABLE_ROW_TAG, namespaces=namespaces)
return rows
return sheet.iterfind(TABLE_ROW_TAG, namespaces=namespaces)
def is_float(cell):
def is_float(cell: etree._Element) -> bool:
return (
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)
if text is None:
return None, 0
value = text.text
value: Union[str, float] = "".join(text.itertext())
if parsed and is_float(cell):
value = float(value)
n_repeated = cell.attrib.get(
_n_repeated = cell.attrib.get(
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

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)
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)):
raise ValueError("Sheet id has to be either `str` or `int`")
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:
raise KeyError("There is no sheet named {}".format(sheet_id))
sheet_id = sheets.index(sheet_id) + 1
sheet = doc.sheets[sheet_id - 1]
sheet: ezodf.Sheet = doc.sheets[sheet_id - 1]
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

View File

View File

@ -1,5 +1,7 @@
"""Provides utility functions for the parser"""
import pandas as pd
def ods_info(doc):
"""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):
"""Drop empty rows and columns from the DataFrame and returns it"""
def sanitize_df(df: pd.DataFrame) -> pd.DataFrame:
"""Drop empty rows and columns from the DataFrame and return it."""
# Delete empty rows
for i in df.index.tolist()[-1::-1]:
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"""
from pathlib import Path
import pandas as pd
@ -16,132 +17,161 @@ duplicated_column_names_file = "example_duplicated_column_names.ods"
col_len_file = "example_col_lengths.ods"
missing_header_file = "example_missing_header.ods"
mixed_dtypes_file = "mixed_dtypes.ods"
skiprows_file = "example_skiprows.ods"
class TestOdsReader:
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_header_file_simple(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_header_file_simple(suffix: str) -> None:
"""Test a simple file with headers."""
path = rsc / header_file
df = read_ods(path.with_suffix(suffix))
path = rsc / header_file
df = read_ods(path.with_suffix(suffix))
assert isinstance(df, pd.DataFrame)
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"])
def test_header_file_with_int(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
df = read_ods(path.with_suffix(suffix), 1)
assert isinstance(df, pd.DataFrame)
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"])
def test_header_file_with_str(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
df = read_ods(path.with_suffix(suffix), "Sheet1")
assert isinstance(df, pd.DataFrame)
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"])
def test_header_file_with_cols(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
columns = ["One", "Two", "Three", "Four", "Five"]
df = read_ods(path.with_suffix(suffix), "Sheet1", columns=columns)
assert list(df.columns) == columns
assert len(df) == 10
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"])
def test_no_header_file_no_cols(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
df = read_ods(path.with_suffix(suffix), 1, headers=False)
assert list(df.columns) == [f"column.{i}" for i in range(len(df.columns))]
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"])
def test_no_header_file_with_cols(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
columns = ["A", "B", "C", "D", "E"]
df = read_ods(path.with_suffix(suffix), 1, headers=False, columns=columns)
assert list(df.columns) == columns
assert len(df) == 10
assert list(df.columns) == columns
assert len(df) == 10
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_duplicated_column_names(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
df = read_ods(path.with_suffix(suffix), 1)
assert isinstance(df, pd.DataFrame)
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"])
def test_header_file_col_len(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
df = read_ods(path.with_suffix(suffix), 1)
assert isinstance(df, pd.DataFrame)
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"])
def test_wrong_id_type(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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"])
def test_non_existent_sheet(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
sheet_name = "No_Sheet"
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}")
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"])
def test_missing_header(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
df = read_ods(path.with_suffix(suffix), 1)
assert isinstance(df, pd.DataFrame)
assert len(df) == 10
assert len(df.columns) == 5
assert isinstance(df, pd.DataFrame)
assert len(df) == 10
assert len(df.columns) == 5
assert df.columns[2] == "unnamed.1"
assert df.columns[2] == "unnamed.1"
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_mixed_dtypes(self, suffix):
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
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
df = read_ods(path.with_suffix(suffix), 1)
assert isinstance(df, pd.DataFrame)
assert len(df) == 10
assert len(df.columns) == 5
assert isinstance(df, pd.DataFrame)
assert len(df) == 10
assert len(df.columns) == 5
type_list = [float, object, float, float, object]
assert df.dtypes.tolist() == type_list
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
col_b_types = [type(v) for v in df.B.values]
assert str in col_b_types and float in col_b_types
@pytest.mark.parametrize("suffix", [".ods", ".fods"])
def test_skiprows(suffix: str) -> None:
"""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)