├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── lint.yml │ ├── moban-update.yml │ ├── pythonpublish.yml │ └── tests.yml ├── .gitignore ├── .isort.cfg ├── .moban.d ├── custom_README.rst.jj2 ├── custom_setup.py.jj2 ├── requirements.txt └── tests │ ├── requirements.txt │ └── test_formatters.py ├── .moban.yml ├── .readthedocs.yml ├── CHANGELOG.rst ├── CONTRIBUTORS.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── changelog.yml ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ └── index.rst ├── format.sh ├── lint.sh ├── pyexcel-ods3.yml ├── pyexcel_ods3 ├── __init__.py ├── odsr.py └── odsw.py ├── requirements.txt ├── rnd_requirements.txt ├── setup.py ├── test.bat ├── test.sh └── tests ├── base.py ├── fixtures ├── 12_day_as_time.ods ├── currency_without_currency.ods ├── file_with_an_empty_sheet.ods ├── multilineods.ods └── ods_formats.ods ├── requirements.txt ├── test_bug_fixes.py ├── test_filter.py ├── test_formatters.py ├── test_multiline_feature.py ├── test_multiple_sheets.py ├── test_ods_reader.py ├── test_stringio.py └── test_writer.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: chfw 4 | patreon: chfw 5 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | With your PR, here is a check list: 2 | 3 | - [ ] Has test cases written? 4 | - [ ] Has all code lines tested? 5 | - [ ] Has `make format` been run? 6 | - [ ] Please update CHANGELOG.yml(not CHANGELOG.rst) 7 | - [ ] Has fair amount of documentation if your change is complex 8 | - [ ] Agree on NEW BSD License for your contribution 9 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | name: lint code 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 12 | uses: actions/setup-python@v1 13 | with: 14 | python-version: 3.11 15 | - name: lint 16 | run: | 17 | pip --use-deprecated=legacy-resolver install flake8 18 | pip --use-deprecated=legacy-resolver install -r tests/requirements.txt 19 | flake8 --exclude=.moban.d,docs,setup.py --builtins=unicode,xrange,long . 20 | python setup.py checkdocs 21 | -------------------------------------------------------------------------------- /.github/workflows/moban-update.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | run_moban: 5 | runs-on: ubuntu-latest 6 | name: synchronize templates via moban 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | ref: ${{ github.head_ref }} 11 | token: ${{ secrets.PAT }} 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.11' 16 | - name: check changes 17 | run: | 18 | pip install markupsafe==2.0.1 19 | pip install ruamel.yaml moban gitfs2 pypifs moban-jinja2-github moban-ansible 20 | moban 21 | git status 22 | git diff --exit-code 23 | - name: Auto-commit 24 | if: failure() 25 | uses: stefanzweifel/git-auto-commit-action@v4 26 | with: 27 | commit_message: >- 28 | This is an auto-commit, updating project meta data, 29 | such as changelog.rst, contributors.rst 30 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | pypi-publish: 9 | name: upload release to PyPI 10 | runs-on: ubuntu-latest 11 | # Specifying a GitHub environment is optional, but strongly encouraged 12 | environment: pypi 13 | permissions: 14 | # IMPORTANT: this permission is mandatory for trusted publishing 15 | id-token: write 16 | steps: 17 | # retrieve your distributions here 18 | - uses: actions/checkout@v1 19 | - name: Set up Python 20 | uses: actions/setup-python@v1 21 | with: 22 | python-version: '3.x' 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install setuptools wheel 27 | - name: Build 28 | run: | 29 | python setup.py sdist bdist_wheel 30 | - name: Publish package distributions to PyPI 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests on Windows, Ubuntu and Mac 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | test: 8 | name: ${{ matrix.os }} / ${{ matrix.python_version }} 9 | runs-on: ${{ matrix.os }}-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: [Ubuntu] 14 | python_version: ["3.9.16"] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python_version }} 22 | architecture: x64 23 | 24 | - name: install 25 | run: | 26 | pip --use-deprecated=legacy-resolver install -r requirements.txt 27 | pip --use-deprecated=legacy-resolver install -r tests/requirements.txt 28 | pip --use-deprecated=legacy-resolver install -r rnd_requirements.txt 29 | - name: test 30 | run: | 31 | pip freeze 32 | nosetests --verbosity=3 --with-coverage --cover-package pyexcel_ods3 --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_ods3 33 | - name: Upload coverage 34 | uses: codecov/codecov-action@v1 35 | with: 36 | name: ${{ matrix.os }} Python ${{ matrix.python-version }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # moban hashes 2 | .moban.hashes 3 | 4 | # Extra rules from https://github.com/github/gitignore/ 5 | # Python rules 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # Cython debug symbols 143 | cython_debug/ 144 | 145 | # VirtualEnv rules 146 | # Virtualenv 147 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 148 | .Python 149 | [Bb]in 150 | [Ii]nclude 151 | [Ll]ib 152 | [Ll]ib64 153 | [Ll]ocal 154 | [Ss]cripts 155 | pyvenv.cfg 156 | .venv 157 | pip-selfcheck.json 158 | 159 | # Linux rules 160 | *~ 161 | 162 | # temporary files which can be created if a process still has a handle open of a deleted file 163 | .fuse_hidden* 164 | 165 | # KDE directory preferences 166 | .directory 167 | 168 | # Linux trash folder which might appear on any partition or disk 169 | .Trash-* 170 | 171 | # .nfs files are created when an open file is removed but is still being accessed 172 | .nfs* 173 | 174 | # Windows rules 175 | # Windows thumbnail cache files 176 | Thumbs.db 177 | Thumbs.db:encryptable 178 | ehthumbs.db 179 | ehthumbs_vista.db 180 | 181 | # Dump file 182 | *.stackdump 183 | 184 | # Folder config file 185 | [Dd]esktop.ini 186 | 187 | # Recycle Bin used on file shares 188 | $RECYCLE.BIN/ 189 | 190 | # Windows Installer files 191 | *.cab 192 | *.msi 193 | *.msix 194 | *.msm 195 | *.msp 196 | 197 | # Windows shortcuts 198 | *.lnk 199 | 200 | # macOS rules 201 | # General 202 | .DS_Store 203 | .AppleDouble 204 | .LSOverride 205 | 206 | # Icon must end with two \r 207 | Icon 208 | 209 | 210 | # Thumbnails 211 | ._* 212 | 213 | # Files that might appear in the root of a volume 214 | .DocumentRevisions-V100 215 | .fseventsd 216 | .Spotlight-V100 217 | .TemporaryItems 218 | .Trashes 219 | .VolumeIcon.icns 220 | .com.apple.timemachine.donotpresent 221 | 222 | # Directories potentially created on remote AFP share 223 | .AppleDB 224 | .AppleDesktop 225 | Network Trash Folder 226 | Temporary Items 227 | .apdisk 228 | 229 | # Emacs rules 230 | # -*- mode: gitignore; -*- 231 | *~ 232 | \#*\# 233 | /.emacs.desktop 234 | /.emacs.desktop.lock 235 | *.elc 236 | auto-save-list 237 | tramp 238 | .\#* 239 | 240 | # Org-mode 241 | .org-id-locations 242 | *_archive 243 | 244 | # flymake-mode 245 | *_flymake.* 246 | 247 | # eshell files 248 | /eshell/history 249 | /eshell/lastdir 250 | 251 | # elpa packages 252 | /elpa/ 253 | 254 | # reftex files 255 | *.rel 256 | 257 | # AUCTeX auto folder 258 | /auto/ 259 | 260 | # cask packages 261 | .cask/ 262 | dist/ 263 | 264 | # Flycheck 265 | flycheck_*.el 266 | 267 | # server auth directory 268 | /server/ 269 | 270 | # projectiles files 271 | .projectile 272 | 273 | # directory configuration 274 | .dir-locals.el 275 | 276 | # network security 277 | /network-security.data 278 | 279 | 280 | # Vim rules 281 | # Swap 282 | [._]*.s[a-v][a-z] 283 | !*.svg # comment out if you don't need vector files 284 | [._]*.sw[a-p] 285 | [._]s[a-rt-v][a-z] 286 | [._]ss[a-gi-z] 287 | [._]sw[a-p] 288 | 289 | # Session 290 | Session.vim 291 | Sessionx.vim 292 | 293 | # Temporary 294 | .netrwhist 295 | *~ 296 | # Auto-generated tag files 297 | tags 298 | # Persistent undo 299 | [._]*.un~ 300 | 301 | # JetBrains rules 302 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 303 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 304 | 305 | # User-specific stuff 306 | .idea/**/workspace.xml 307 | .idea/**/tasks.xml 308 | .idea/**/usage.statistics.xml 309 | .idea/**/dictionaries 310 | .idea/**/shelf 311 | 312 | # Generated files 313 | .idea/**/contentModel.xml 314 | 315 | # Sensitive or high-churn files 316 | .idea/**/dataSources/ 317 | .idea/**/dataSources.ids 318 | .idea/**/dataSources.local.xml 319 | .idea/**/sqlDataSources.xml 320 | .idea/**/dynamic.xml 321 | .idea/**/uiDesigner.xml 322 | .idea/**/dbnavigator.xml 323 | 324 | # Gradle 325 | .idea/**/gradle.xml 326 | .idea/**/libraries 327 | 328 | # Gradle and Maven with auto-import 329 | # When using Gradle or Maven with auto-import, you should exclude module files, 330 | # since they will be recreated, and may cause churn. Uncomment if using 331 | # auto-import. 332 | # .idea/artifacts 333 | # .idea/compiler.xml 334 | # .idea/jarRepositories.xml 335 | # .idea/modules.xml 336 | # .idea/*.iml 337 | # .idea/modules 338 | # *.iml 339 | # *.ipr 340 | 341 | # CMake 342 | cmake-build-*/ 343 | 344 | # Mongo Explorer plugin 345 | .idea/**/mongoSettings.xml 346 | 347 | # File-based project format 348 | *.iws 349 | 350 | # IntelliJ 351 | out/ 352 | 353 | # mpeltonen/sbt-idea plugin 354 | .idea_modules/ 355 | 356 | # JIRA plugin 357 | atlassian-ide-plugin.xml 358 | 359 | # Cursive Clojure plugin 360 | .idea/replstate.xml 361 | 362 | # Crashlytics plugin (for Android Studio and IntelliJ) 363 | com_crashlytics_export_strings.xml 364 | crashlytics.properties 365 | crashlytics-build.properties 366 | fabric.properties 367 | 368 | # Editor-based Rest Client 369 | .idea/httpRequests 370 | 371 | # Android studio 3.1+ serialized cache file 372 | .idea/caches/build_file_checksums.ser 373 | 374 | # SublimeText rules 375 | # Cache files for Sublime Text 376 | *.tmlanguage.cache 377 | *.tmPreferences.cache 378 | *.stTheme.cache 379 | 380 | # Workspace files are user-specific 381 | *.sublime-workspace 382 | 383 | # Project files should be checked into the repository, unless a significant 384 | # proportion of contributors will probably not be using Sublime Text 385 | # *.sublime-project 386 | 387 | # SFTP configuration file 388 | sftp-config.json 389 | sftp-config-alt*.json 390 | 391 | # Package control specific files 392 | Package Control.last-run 393 | Package Control.ca-list 394 | Package Control.ca-bundle 395 | Package Control.system-ca-bundle 396 | Package Control.cache/ 397 | Package Control.ca-certs/ 398 | Package Control.merged-ca-bundle 399 | Package Control.user-ca-bundle 400 | oscrypto-ca-bundle.crt 401 | bh_unicode_properties.cache 402 | 403 | # Sublime-github package stores a github token in this file 404 | # https://packagecontrol.io/packages/sublime-github 405 | GitHub.sublime-settings 406 | 407 | # KDevelop4 rules 408 | *.kdev4 409 | .kdev4/ 410 | 411 | # Kate rules 412 | # Swap Files # 413 | .*.kate-swp 414 | .swp.* 415 | 416 | # TextMate rules 417 | *.tmproj 418 | *.tmproject 419 | tmtags 420 | 421 | # VisualStudioCode rules 422 | .vscode/* 423 | !.vscode/settings.json 424 | !.vscode/tasks.json 425 | !.vscode/launch.json 426 | !.vscode/extensions.json 427 | *.code-workspace 428 | 429 | # Local History for Visual Studio Code 430 | .history/ 431 | 432 | # Xcode rules 433 | # Xcode 434 | # 435 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 436 | 437 | ## User settings 438 | xcuserdata/ 439 | 440 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 441 | *.xcscmblueprint 442 | *.xccheckout 443 | 444 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 445 | build/ 446 | DerivedData/ 447 | *.moved-aside 448 | *.pbxuser 449 | !default.pbxuser 450 | *.mode1v3 451 | !default.mode1v3 452 | *.mode2v3 453 | !default.mode2v3 454 | *.perspectivev3 455 | !default.perspectivev3 456 | 457 | ## Gcc Patch 458 | /*.gcno 459 | 460 | # Eclipse rules 461 | .metadata 462 | bin/ 463 | tmp/ 464 | *.tmp 465 | *.bak 466 | *.swp 467 | *~.nib 468 | local.properties 469 | .settings/ 470 | .loadpath 471 | .recommenders 472 | 473 | # External tool builders 474 | .externalToolBuilders/ 475 | 476 | # Locally stored "Eclipse launch configurations" 477 | *.launch 478 | 479 | # PyDev specific (Python IDE for Eclipse) 480 | *.pydevproject 481 | 482 | # CDT-specific (C/C++ Development Tooling) 483 | .cproject 484 | 485 | # CDT- autotools 486 | .autotools 487 | 488 | # Java annotation processor (APT) 489 | .factorypath 490 | 491 | # PDT-specific (PHP Development Tools) 492 | .buildpath 493 | 494 | # sbteclipse plugin 495 | .target 496 | 497 | # Tern plugin 498 | .tern-project 499 | 500 | # TeXlipse plugin 501 | .texlipse 502 | 503 | # STS (Spring Tool Suite) 504 | .springBeans 505 | 506 | # Code Recommenders 507 | .recommenders/ 508 | 509 | # Annotation Processing 510 | .apt_generated/ 511 | .apt_generated_test/ 512 | 513 | # Scala IDE specific (Scala & Java development for Eclipse) 514 | .cache-main 515 | .scala_dependencies 516 | .worksheet 517 | 518 | # Uncomment this line if you wish to ignore the project description file. 519 | # Typically, this file would be tracked if it contains build/dependency configurations: 520 | #.project 521 | 522 | # TortoiseGit rules 523 | # Project-level settings 524 | /.tgitconfig 525 | 526 | # Tags rules 527 | # Ignore tags created by etags, ctags, gtags (GNU global) and cscope 528 | TAGS 529 | .TAGS 530 | !TAGS/ 531 | tags 532 | .tags 533 | !tags/ 534 | gtags.files 535 | GTAGS 536 | GRTAGS 537 | GPATH 538 | GSYMS 539 | cscope.files 540 | cscope.out 541 | cscope.in.out 542 | cscope.po.out 543 | 544 | 545 | # remove moban hash dictionary 546 | .moban.hashes 547 | 548 | docs/build/ 549 | commons 550 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=79 3 | known_first_party=pyexcel, ezodf 4 | known_third_party=mock,nose 5 | indent=' ' 6 | multi_line_output=3 7 | length_sort=1 8 | default_section=FIRSTPARTY 9 | no_lines_before=LOCALFOLDER 10 | sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER 11 | -------------------------------------------------------------------------------- /.moban.d/custom_README.rst.jj2: -------------------------------------------------------------------------------- 1 | {%extends 'README.rst.jj2' %} 2 | 3 | 4 | {%block documentation_link%} 5 | {%endblock%} 6 | 7 | {%block description%} 8 | **pyexcel-ods3** is a tiny wrapper library to read, manipulate and write data in ods 9 | format. You are likely to use `pyexcel `__ together 10 | with this library. `pyexcel-ods `__ is a sister 11 | library that depends on GPL licensed odfpy. 12 | `pyexcel-odsr `_ is the other sister library 13 | that has no external dependency but do ods reading only 14 | {%endblock%} 15 | 16 | {%block middle_block%} 17 | .. testcode:: 18 | :hide: 19 | 20 | >>> notneeded=io.seek(0) 21 | {%endblock%} 22 | 23 | {% block pagination_note%} 24 | Special notice 30/01/2017: due to the constraints of the underlying 3rd party 25 | library, it will read the whole file before returning the paginated data. So 26 | at the end of day, the only benefit is less data returned from the reading 27 | function. No major performance improvement will be seen. 28 | 29 | With that said, please install `pyexcel-odsr `_ 30 | and it gives better performance in pagination. 31 | {%endblock%} 32 | 33 | {%block extras %} 34 | Installation Note 35 | ================================================================================ 36 | The installation of `lxml` will be tricky on Windows platform. It is recommended that you download a lxml's own windows installer instead of using pip. 37 | {%endblock%} 38 | -------------------------------------------------------------------------------- /.moban.d/custom_setup.py.jj2: -------------------------------------------------------------------------------- 1 | {% extends 'pyexcel-setup.py.jj2' %} 2 | 3 | {%block platform_block%} 4 | {%endblock%} 5 | 6 | {%block additional_keywords%} 7 | 'ods' 8 | {%endblock%} 9 | -------------------------------------------------------------------------------- /.moban.d/requirements.txt: -------------------------------------------------------------------------------- 1 | {% for dependency in dependencies: %} 2 | {{dependency}} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /.moban.d/tests/requirements.txt: -------------------------------------------------------------------------------- 1 | {% extends 'tests/requirements.txt.jj2' %} 2 | {%block extras %} 3 | moban 4 | black;python_version>="3.6" 5 | isort;python_version>="3.6" 6 | psutil 7 | pyexcel 8 | pyexcel-xls 9 | {%endblock%} 10 | -------------------------------------------------------------------------------- /.moban.d/tests/test_formatters.py: -------------------------------------------------------------------------------- 1 | {% extends 'tests/test_formatters.py.jj2' %} 2 | 3 | {% block test_date_format %} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /.moban.yml: -------------------------------------------------------------------------------- 1 | overrides: "git://github.com/pyexcel/pyexcel-mobans!/mobanfile.yaml" 2 | configuration: 3 | configuration: pyexcel-ods3.yml 4 | targets: 5 | - README.rst: custom_README.rst.jj2 6 | - setup.py: custom_setup.py.jj2 7 | - "docs/source/conf.py": "docs/source/conf.py.jj2" 8 | - MANIFEST.in: MANIFEST.in.jj2 9 | - "tests/requirements.txt": "tests/requirements.txt" 10 | - .gitignore: commons-gitignore.jj2 11 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | 13 | # Build documentation in the docs/ directory with Sphinx 14 | sphinx: 15 | configuration: docs/source/conf.py 16 | 17 | # Optionally build your docs in additional formats such as PDF 18 | formats: 19 | - pdf 20 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Change log 2 | ================================================================================ 3 | 4 | 0.6.2 - 10.7.2024 5 | -------------------------------------------------------------------------------- 6 | 7 | **added** 8 | 9 | #. `#36 `_: fixing get_data 10 | for a currency cell with no currency defined 11 | 12 | 0.6.1 - 1.2.2022 13 | -------------------------------------------------------------------------------- 14 | 15 | **added** 16 | 17 | #. `#28 `_: support datetime 18 | 19 | 0.6.0 - 8.10.2020 20 | -------------------------------------------------------------------------------- 21 | 22 | **added** 23 | 24 | #. new style reader and writer plugins. works with pyexcel-io v0.6.2 25 | 26 | 0.5.3 - 27.11.2018 27 | -------------------------------------------------------------------------------- 28 | 29 | **added** 30 | 31 | #. `pyexcel#57 `_, long type will 32 | not be written in ods. please use string type. And if the integer is equal or 33 | greater than 10 to the power of 16, it will not be written either in ods. In 34 | both situation, IntegerPrecisionLossError will be raised. 35 | 36 | 0.5.2 - 23.10.2017 37 | -------------------------------------------------------------------------------- 38 | 39 | **updated** 40 | 41 | #. pyexcel `pyexcel#105 `_, 42 | remove gease from setup_requires, introduced by 0.5.1. 43 | #. remove python2.6 test support 44 | #. update its dependecy on pyexcel-io to 0.5.3 45 | 46 | 0.5.1 - 20.10.2017 47 | -------------------------------------------------------------------------------- 48 | 49 | **added** 50 | 51 | #. `pyexcel#103 `_, include 52 | LICENSE file in MANIFEST.in, meaning LICENSE file will appear in the released 53 | tar ball. 54 | 55 | 0.5.0 - 30.08.2017 56 | -------------------------------------------------------------------------------- 57 | 58 | **Updated** 59 | 60 | #. put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO. 61 | Hence, there will be performance boost in handling files in memory. 62 | 63 | **Relocated** 64 | 65 | #. All ods type conversion code lives in pyexcel_io.service module 66 | 67 | 0.4.1 - 17.08.2017 68 | -------------------------------------------------------------------------------- 69 | 70 | **Updated** 71 | 72 | #. update dependency to use pyexcel-ezodf v0.3.3 as ezodf 0.3.2 has `the bug 73 | `_, cannot handle file 74 | alike objects and has not been updated for 2 years. 75 | 76 | 0.4.0 - 19.06.2017 77 | -------------------------------------------------------------------------------- 78 | 79 | **Updated** 80 | 81 | #. `pyexcel#14 `_, close file 82 | handle 83 | #. pyexcel-io plugin interface now updated to use `lml 84 | `_. 85 | 86 | 0.3.2 - 13.04.2017 87 | -------------------------------------------------------------------------------- 88 | 89 | **Updated** 90 | 91 | #. issue `pyexcel#8 `_, 92 | PT288H00M00S is valid duration 93 | 94 | 0.3.1 - 02.02.2017 95 | -------------------------------------------------------------------------------- 96 | 97 | **Added** 98 | 99 | #. Recognize currency type 100 | 101 | 0.3.0 - 22.12.2016 102 | -------------------------------------------------------------------------------- 103 | 104 | **Updated** 105 | 106 | #. Code refactoring with pyexcel-io v 0.3.0 107 | 108 | 0.2.2 - 05.11.2016 109 | -------------------------------------------------------------------------------- 110 | 111 | **Updated** 112 | 113 | #. `pyexcel#11 `_, be able to 114 | consume a generator of two dimensional arrays. 115 | 116 | 0.2.1 - 31.08.2016 117 | -------------------------------------------------------------------------------- 118 | 119 | **Added** 120 | 121 | #. support pagination. two pairs: start_row, row_limit and start_column, 122 | column_limit help you deal with large files. 123 | 124 | 0.2.0 - 01.06.2016 125 | -------------------------------------------------------------------------------- 126 | 127 | **Added** 128 | 129 | #. By default, `float` will be converted to `int` where fits. `auto_detect_int`, 130 | a flag to switch off the autoatic conversion from `float` to `int`. 131 | #. 'library=pyexcel-ods3' was added so as to inform pyexcel to use it instead of 132 | other libraries, in the situation where multiple plugins for the same file 133 | type are installed 134 | 135 | **Updated** 136 | 137 | #. support the auto-import feature of pyexcel-io 0.2.0 138 | #. compatibility with pyexcel-io 0.1.0 139 | 140 | 0.1.0 - 17.01.2016 141 | -------------------------------------------------------------------------------- 142 | 143 | **Updated** 144 | 145 | #. support the auto-import feature of pyexcel-io 0.2.0 146 | #. compatibility with pyexcel-io 0.1.0 147 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 contributors 4 | ================================================================================ 5 | 6 | In alphabetical order: 7 | 8 | * `Benedikt Waldvogel `_ 9 | * `Mateusz Konieczny `_ 10 | * `Michael K. `_ 11 | * `Michael Leiseca `_ 12 | * `Stephen J. Fuhry `_ 13 | * `Vincent Raspal `_ 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2025 by Onni Software Ltd. and its contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms of the software as well 5 | as documentation, with or without modification, are permitted provided 6 | that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of 'pyexcel-ods3' nor the names of the contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 20 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 21 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 23 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 24 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 25 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 26 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 28 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 30 | DAMAGE. 31 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include CHANGELOG.rst 4 | include CONTRIBUTORS.rst 5 | recursive-include tests * 6 | recursive-include docs * 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | test: lint 4 | bash test.sh 5 | 6 | install_test: 7 | pip install -r tests/requirements.txt 8 | 9 | lint: 10 | bash lint.sh 11 | 12 | format: 13 | bash format.sh 14 | 15 | git-diff-check: 16 | git diff --exit-code 17 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | pyexcel-ods3 - Let you focus on data, instead of ods format 3 | ================================================================================ 4 | 5 | .. image:: https://raw.githubusercontent.com/pyexcel/pyexcel.github.io/master/images/patreon.png 6 | :target: https://www.patreon.com/chfw 7 | 8 | .. image:: https://raw.githubusercontent.com/pyexcel/pyexcel-mobans/master/images/awesome-badge.svg 9 | :target: https://awesome-python.com/#specific-formats-processing 10 | 11 | .. image:: https://codecov.io/gh/pyexcel/pyexcel-ods3/branch/master/graph/badge.svg 12 | :target: https://codecov.io/gh/pyexcel/pyexcel-ods3 13 | 14 | .. image:: https://badge.fury.io/py/pyexcel-ods3.svg 15 | :target: https://pypi.org/project/pyexcel-ods3 16 | 17 | .. image:: https://anaconda.org/conda-forge/pyexcel-ods3/badges/version.svg 18 | :target: https://anaconda.org/conda-forge/pyexcel-ods3 19 | 20 | 21 | .. image:: https://pepy.tech/badge/pyexcel-ods3/month 22 | :target: https://pepy.tech/project/pyexcel-ods3 23 | 24 | .. image:: https://anaconda.org/conda-forge/pyexcel-ods3/badges/downloads.svg 25 | :target: https://anaconda.org/conda-forge/pyexcel-ods3 26 | 27 | .. image:: https://img.shields.io/gitter/room/gitterHQ/gitter.svg 28 | :target: https://gitter.im/pyexcel/Lobby 29 | 30 | .. image:: https://img.shields.io/static/v1?label=continuous%20templating&message=%E6%A8%A1%E7%89%88%E6%9B%B4%E6%96%B0&color=blue&style=flat-square 31 | :target: https://moban.readthedocs.io/en/latest/#at-scale-continous-templating-for-open-source-projects 32 | 33 | .. image:: https://img.shields.io/static/v1?label=coding%20style&message=black&color=black&style=flat-square 34 | :target: https://github.com/psf/black 35 | 36 | **pyexcel-ods3** is a tiny wrapper library to read, manipulate and write data in ods 37 | format. You are likely to use `pyexcel `__ together 38 | with this library. `pyexcel-ods `__ is a sister 39 | library that depends on GPL licensed odfpy. 40 | `pyexcel-odsr `_ is the other sister library 41 | that has no external dependency but do ods reading only 42 | 43 | Support the project 44 | ================================================================================ 45 | 46 | If your company uses pyexcel and its components in a revenue-generating product, 47 | please consider supporting the project on GitHub or 48 | `Patreon `_. Your financial 49 | support will enable me to dedicate more time to coding, improving documentation, 50 | and creating engaging content. 51 | 52 | 53 | Known constraints 54 | ================== 55 | 56 | Fonts, colors and charts are not supported. 57 | 58 | Nor to read password protected xls, xlsx and ods files. 59 | 60 | Installation 61 | ================================================================================ 62 | 63 | 64 | You can install pyexcel-ods3 via pip: 65 | 66 | .. code-block:: bash 67 | 68 | $ pip install pyexcel-ods3 69 | 70 | 71 | or clone it and install it: 72 | 73 | .. code-block:: bash 74 | 75 | $ git clone https://github.com/pyexcel/pyexcel-ods3.git 76 | $ cd pyexcel-ods3 77 | $ python setup.py install 78 | 79 | Usage 80 | ================================================================================ 81 | 82 | As a standalone library 83 | -------------------------------------------------------------------------------- 84 | 85 | .. testcode:: 86 | :hide: 87 | 88 | >>> import os 89 | >>> import sys 90 | >>> from io import BytesIO 91 | >>> from collections import OrderedDict 92 | 93 | 94 | Write to an ods file 95 | ******************************************************************************** 96 | 97 | 98 | 99 | Here's the sample code to write a dictionary to an ods file: 100 | 101 | .. code-block:: python 102 | 103 | >>> from pyexcel_ods3 import save_data 104 | >>> data = OrderedDict() # from collections import OrderedDict 105 | >>> data.update({"Sheet 1": [[1, 2, 3], [4, 5, 6]]}) 106 | >>> data.update({"Sheet 2": [["row 1", "row 2", "row 3"]]}) 107 | >>> save_data("your_file.ods", data) 108 | 109 | 110 | Read from an ods file 111 | ******************************************************************************** 112 | 113 | Here's the sample code: 114 | 115 | .. code-block:: python 116 | 117 | >>> from pyexcel_ods3 import get_data 118 | >>> data = get_data("your_file.ods") 119 | >>> import json 120 | >>> print(json.dumps(data)) 121 | {"Sheet 1": [[1, 2, 3], [4, 5, 6]], "Sheet 2": [["row 1", "row 2", "row 3"]]} 122 | 123 | 124 | Write an ods to memory 125 | ******************************************************************************** 126 | 127 | Here's the sample code to write a dictionary to an ods file: 128 | 129 | .. code-block:: python 130 | 131 | >>> from pyexcel_ods3 import save_data 132 | >>> data = OrderedDict() 133 | >>> data.update({"Sheet 1": [[1, 2, 3], [4, 5, 6]]}) 134 | >>> data.update({"Sheet 2": [[7, 8, 9], [10, 11, 12]]}) 135 | >>> io = BytesIO() 136 | >>> save_data(io, data) 137 | >>> # do something with the io 138 | >>> # In reality, you might give it to your http response 139 | >>> # object for downloading 140 | 141 | 142 | 143 | .. testcode:: 144 | :hide: 145 | 146 | >>> notneeded=io.seek(0) 147 | 148 | Read from an ods from memory 149 | ******************************************************************************** 150 | 151 | Continue from previous example: 152 | 153 | .. code-block:: python 154 | 155 | >>> # This is just an illustration 156 | >>> # In reality, you might deal with ods file upload 157 | >>> # where you will read from requests.FILES['YOUR_ODS_FILE'] 158 | >>> data = get_data(io) 159 | >>> print(json.dumps(data)) 160 | {"Sheet 1": [[1, 2, 3], [4, 5, 6]], "Sheet 2": [[7, 8, 9], [10, 11, 12]]} 161 | 162 | 163 | Pagination feature 164 | ******************************************************************************** 165 | 166 | Special notice 30/01/2017: due to the constraints of the underlying 3rd party 167 | library, it will read the whole file before returning the paginated data. So 168 | at the end of day, the only benefit is less data returned from the reading 169 | function. No major performance improvement will be seen. 170 | 171 | With that said, please install `pyexcel-odsr `_ 172 | and it gives better performance in pagination. 173 | 174 | Let's assume the following file is a huge ods file: 175 | 176 | .. code-block:: python 177 | 178 | >>> huge_data = [ 179 | ... [1, 21, 31], 180 | ... [2, 22, 32], 181 | ... [3, 23, 33], 182 | ... [4, 24, 34], 183 | ... [5, 25, 35], 184 | ... [6, 26, 36] 185 | ... ] 186 | >>> sheetx = { 187 | ... "huge": huge_data 188 | ... } 189 | >>> save_data("huge_file.ods", sheetx) 190 | 191 | And let's pretend to read partial data: 192 | 193 | .. code-block:: python 194 | 195 | >>> partial_data = get_data("huge_file.ods", start_row=2, row_limit=3) 196 | >>> print(json.dumps(partial_data)) 197 | {"huge": [[3, 23, 33], [4, 24, 34], [5, 25, 35]]} 198 | 199 | And you could as well do the same for columns: 200 | 201 | .. code-block:: python 202 | 203 | >>> partial_data = get_data("huge_file.ods", start_column=1, column_limit=2) 204 | >>> print(json.dumps(partial_data)) 205 | {"huge": [[21, 31], [22, 32], [23, 33], [24, 34], [25, 35], [26, 36]]} 206 | 207 | Obvious, you could do both at the same time: 208 | 209 | .. code-block:: python 210 | 211 | >>> partial_data = get_data("huge_file.ods", 212 | ... start_row=2, row_limit=3, 213 | ... start_column=1, column_limit=2) 214 | >>> print(json.dumps(partial_data)) 215 | {"huge": [[23, 33], [24, 34], [25, 35]]} 216 | 217 | .. testcode:: 218 | :hide: 219 | 220 | >>> os.unlink("huge_file.ods") 221 | 222 | 223 | As a pyexcel plugin 224 | -------------------------------------------------------------------------------- 225 | 226 | No longer, explicit import is needed since pyexcel version 0.2.2. Instead, 227 | this library is auto-loaded. So if you want to read data in ods format, 228 | installing it is enough. 229 | 230 | 231 | Reading from an ods file 232 | ******************************************************************************** 233 | 234 | Here is the sample code: 235 | 236 | .. code-block:: python 237 | 238 | >>> import pyexcel as pe 239 | >>> sheet = pe.get_book(file_name="your_file.ods") 240 | >>> sheet 241 | Sheet 1: 242 | +---+---+---+ 243 | | 1 | 2 | 3 | 244 | +---+---+---+ 245 | | 4 | 5 | 6 | 246 | +---+---+---+ 247 | Sheet 2: 248 | +-------+-------+-------+ 249 | | row 1 | row 2 | row 3 | 250 | +-------+-------+-------+ 251 | 252 | 253 | Writing to an ods file 254 | ******************************************************************************** 255 | 256 | Here is the sample code: 257 | 258 | .. code-block:: python 259 | 260 | >>> sheet.save_as("another_file.ods") 261 | 262 | 263 | Reading from a IO instance 264 | ******************************************************************************** 265 | 266 | You got to wrap the binary content with stream to get ods working: 267 | 268 | .. code-block:: python 269 | 270 | >>> # This is just an illustration 271 | >>> # In reality, you might deal with ods file upload 272 | >>> # where you will read from requests.FILES['YOUR_ODS_FILE'] 273 | >>> odsfile = "another_file.ods" 274 | >>> with open(odsfile, "rb") as f: 275 | ... content = f.read() 276 | ... r = pe.get_book(file_type="ods", file_content=content) 277 | ... print(r) 278 | ... 279 | Sheet 1: 280 | +---+---+---+ 281 | | 1 | 2 | 3 | 282 | +---+---+---+ 283 | | 4 | 5 | 6 | 284 | +---+---+---+ 285 | Sheet 2: 286 | +-------+-------+-------+ 287 | | row 1 | row 2 | row 3 | 288 | +-------+-------+-------+ 289 | 290 | 291 | Writing to a BytesIO instance 292 | ******************************************************************************** 293 | 294 | You need to pass a BytesIO instance to Writer: 295 | 296 | .. code-block:: python 297 | 298 | >>> data = [ 299 | ... [1, 2, 3], 300 | ... [4, 5, 6] 301 | ... ] 302 | >>> io = BytesIO() 303 | >>> sheet = pe.Sheet(data) 304 | >>> io = sheet.save_to_memory("ods", io) 305 | >>> # then do something with io 306 | >>> # In reality, you might give it to your http response 307 | >>> # object for downloading 308 | 309 | 310 | License 311 | ================================================================================ 312 | 313 | New BSD License 314 | 315 | Developer guide 316 | ================== 317 | 318 | Development steps for code changes 319 | 320 | #. git clone https://github.com/pyexcel/pyexcel-ods3.git 321 | #. cd pyexcel-ods3 322 | 323 | Upgrade your setup tools and pip. They are needed for development and testing only: 324 | 325 | #. pip install --upgrade setuptools pip 326 | 327 | Then install relevant development requirements: 328 | 329 | #. pip install -r rnd_requirements.txt # if such a file exists 330 | #. pip install -r requirements.txt 331 | #. pip install -r tests/requirements.txt 332 | 333 | Once you have finished your changes, please provide test case(s), relevant documentation 334 | and update changelog.yml 335 | 336 | .. note:: 337 | 338 | As to rnd_requirements.txt, usually, it is created when a dependent 339 | library is not released. Once the dependency is installed 340 | (will be released), the future 341 | version of the dependency in the requirements.txt will be valid. 342 | 343 | 344 | How to test your contribution 345 | -------------------------------------------------------------------------------- 346 | 347 | Although `nose` and `doctest` are both used in code testing, it is advisable 348 | that unit tests are put in tests. `doctest` is incorporated only to make sure 349 | the code examples in documentation remain valid across different development 350 | releases. 351 | 352 | On Linux/Unix systems, please launch your tests like this:: 353 | 354 | $ make 355 | 356 | On Windows, please issue this command:: 357 | 358 | > test.bat 359 | 360 | 361 | Before you commit 362 | ------------------------------ 363 | 364 | Please run:: 365 | 366 | $ make format 367 | 368 | so as to beautify your code otherwise your build may fail your unit test. 369 | 370 | 371 | Installation Note 372 | ================================================================================ 373 | The installation of `lxml` will be tricky on Windows platform. It is recommended that you download a lxml's own windows installer instead of using pip. 374 | 375 | .. testcode:: 376 | :hide: 377 | 378 | >>> import os 379 | >>> os.unlink("your_file.ods") 380 | >>> os.unlink("another_file.ods") 381 | -------------------------------------------------------------------------------- /changelog.yml: -------------------------------------------------------------------------------- 1 | name: pyexcel-ods3 2 | organisation: pyexcel 3 | releases: 4 | - changes: 5 | - action: added 6 | details: 7 | - '`#36`: fixing get_data for a currency cell with no currency defined' 8 | date: 10.7.2024 9 | version: 0.6.2 10 | - changes: 11 | - action: added 12 | details: 13 | - '`#28`: support datetime' 14 | date: 1.2.2022 15 | version: 0.6.1 16 | - changes: 17 | - action: added 18 | details: 19 | - 'new style reader and writer plugins. works with pyexcel-io v0.6.2' 20 | date: 8.10.2020 21 | version: 0.6.0 22 | - changes: 23 | - action: added 24 | details: 25 | - '`pyexcel#pyexcel-io#57`, long type will not be written in ods. please use 26 | string type. And if the integer is equal or greater than 10 to the power of 27 | 16, it will not be written either in ods. In both situation, IntegerPrecisionLossError 28 | will be raised.' 29 | date: 27.11.2018 30 | version: 0.5.3 31 | - changes: 32 | - action: updated 33 | details: 34 | - pyexcel `pyexcel#105`, remove gease from setup_requires, introduced by 0.5.1. 35 | - remove python2.6 test support 36 | - update its dependecy on pyexcel-io to 0.5.3 37 | date: 23.10.2017 38 | version: 0.5.2 39 | - changes: 40 | - action: added 41 | details: 42 | - '`pyexcel#103`, include LICENSE file in MANIFEST.in, meaning LICENSE file will 43 | appear in the released tar ball.' 44 | date: 20.10.2017 45 | version: 0.5.1 46 | - changes: 47 | - action: Updated 48 | details: 49 | - put dependency on pyexcel-io 0.5.0, which uses cStringIO instead of StringIO. Hence, 50 | there will be performance boost in handling files in memory. 51 | - action: Relocated 52 | details: 53 | - All ods type conversion code lives in pyexcel_io.service module 54 | date: 30.08.2017 55 | version: 0.5.0 56 | - changes: 57 | - action: Updated 58 | details: 59 | - update dependency to use pyexcel-ezodf v0.3.3 as ezodf 0.3.2 has `the bug `_, 60 | cannot handle file alike objects and has not been updated for 2 years. 61 | date: 17.08.2017 62 | version: 0.4.1 63 | - changes: 64 | - action: Updated 65 | details: 66 | - '`pyexcel#pyexcel-xlsx#14`, close file handle' 67 | - pyexcel-io plugin interface now updated to use `lml `_. 68 | date: 19.06.2017 69 | version: 0.4.0 70 | - changes: 71 | - action: Updated 72 | details: 73 | - issue `pyexcel#pyexcel-ods3#8`, PT288H00M00S is valid duration 74 | date: 13.04.2017 75 | version: 0.3.2 76 | - changes: 77 | - action: Added 78 | details: 79 | - Recognize currency type 80 | date: 02.02.2017 81 | version: 0.3.1 82 | - changes: 83 | - action: Updated 84 | details: 85 | - Code refactoring with pyexcel-io v 0.3.0 86 | date: 22.12.2016 87 | version: 0.3.0 88 | - changes: 89 | - action: Updated 90 | details: 91 | - '`pyexcel#pyexcel-ods3#11`, be able to consume a generator of two dimensional 92 | arrays.' 93 | date: 05.11.2016 94 | version: 0.2.2 95 | - changes: 96 | - action: Added 97 | details: 98 | - 'support pagination. two pairs: start_row, row_limit and start_column, column_limit 99 | help you deal with large files.' 100 | date: 31.08.2016 101 | version: 0.2.1 102 | - changes: 103 | - action: Added 104 | details: 105 | - By default, `float` will be converted to `int` where fits. `auto_detect_int`, a 106 | flag to switch off the autoatic conversion from `float` to `int`. 107 | - '''library=pyexcel-ods3'' was added so as to inform pyexcel to use it instead of 108 | other libraries, in the situation where multiple plugins for the same file 109 | type are installed' 110 | - action: Updated 111 | details: 112 | - support the auto-import feature of pyexcel-io 0.2.0 113 | - compatibility with pyexcel-io 0.1.0 114 | date: 01.06.2016 115 | version: 0.2.0 116 | - changes: 117 | - action: Updated 118 | details: 119 | - support the auto-import feature of pyexcel-io 0.2.0 120 | - compatibility with pyexcel-io 0.1.0 121 | date: 17.01.2016 122 | version: 0.1.0 123 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyexcel-ods3.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyexcel-ods3.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyexcel-ods3" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyexcel-ods3" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyexcel-ods3.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyexcel-ods3.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | DESCRIPTION = ( 3 | 'A wrapper library to read, manipulate and write data in ods format' + 4 | '' 5 | ) 6 | # Configuration file for the Sphinx documentation builder. 7 | # 8 | # This file only contains a selection of the most common options. For a full 9 | # list see the documentation: 10 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 11 | 12 | # -- Path setup -------------------------------------------------------------- 13 | 14 | # If extensions (or modules to document with autodoc) are in another directory, 15 | # add these directories to sys.path here. If the directory is relative to the 16 | # documentation root, use os.path.abspath to make it absolute, like shown here. 17 | # 18 | # import os 19 | # import sys 20 | # sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = 'pyexcel-ods3' 25 | copyright = '2015-2025 Onni Software Ltd.' 26 | author = 'C.W.' 27 | # The short X.Y version 28 | version = '0.6.1' 29 | # The full version, including alpha/beta/rc tags 30 | release = '0.6.1' 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode',] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The language for content autogenerated by Sphinx. Refer to documentation 43 | # for a list of supported languages. 44 | # 45 | # This is also used if you do content translation via gettext catalogs. 46 | # Usually you set "language" from the command line for these cases. 47 | language = 'en' 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = [] 53 | 54 | 55 | # -- Options for HTML output ------------------------------------------------- 56 | 57 | # The theme to use for HTML and HTML Help pages. See the documentation for 58 | # a list of builtin themes. 59 | # 60 | html_theme = 'sphinx_rtd_theme' 61 | 62 | # Add any paths that contain custom static files (such as style sheets) here, 63 | # relative to this directory. They are copied after the builtin static files, 64 | # so a file named "default.css" will overwrite the builtin "default.css". 65 | html_static_path = ['_static'] 66 | 67 | # -- Extension configuration ------------------------------------------------- 68 | # -- Options for intersphinx extension --------------------------------------- 69 | 70 | # Example configuration for intersphinx: refer to the Python standard library. 71 | intersphinx_mapping = {'python': ('https://docs.python.org/3', 72 | 'python-inv.txt')} 73 | # TODO: html_theme not configurable upstream 74 | html_theme = 'default' 75 | 76 | # TODO: DESCRIPTION not configurable upstream 77 | texinfo_documents = [ 78 | ('index', 'pyexcel-ods3', 79 | 'pyexcel-ods3 Documentation', 80 | 'Onni Software Ltd.', 'pyexcel-ods3', 81 | DESCRIPTION, 82 | 'Miscellaneous'), 83 | ] 84 | intersphinx_mapping.update({ 85 | 'pyexcel': ('http://pyexcel.readthedocs.io/en/latest/', None), 86 | }) 87 | master_doc = "index" 88 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pyexcel-ods3 documentation master file, created by 2 | sphinx-quickstart on Sun Dec 13 19:50:31 2015-2016 . 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../../README.rst 7 | 8 | Indices and tables 9 | ================== 10 | 11 | * :ref:`genindex` 12 | * :ref:`modindex` 13 | * :ref:`search` 14 | 15 | -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | isort $(find pyexcel_ods3 -name "*.py"|xargs echo) $(find tests -name "*.py"|xargs echo) 2 | black -l 79 pyexcel_ods3 3 | black -l 79 tests 4 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | pip install flake8 2 | flake8 --exclude=.moban.d,docs,setup.py --builtins=unicode,xrange,long . && python setup.py checkdocs -------------------------------------------------------------------------------- /pyexcel-ods3.yml: -------------------------------------------------------------------------------- 1 | overrides: "pyexcel.yaml" 2 | name: "pyexcel-ods3" 3 | nick_name: ods3 4 | version: 0.6.1 5 | current_version: 0.6.1 6 | release: 0.6.1 7 | file_type: ods 8 | is_on_conda: true 9 | dependencies: 10 | - pyexcel-io>=0.6.2 11 | - lxml 12 | - pyexcel-ezodf>=0.3.3 13 | test_dependencies: 14 | - pyexcel 15 | - psutil 16 | - pyexcel-xls 17 | moban_command: false 18 | description: |- 19 | A wrapper library to read, manipulate and write data in ods format 20 | python_requires: ">=3.6" 21 | min_python_version: "3.6" 22 | -------------------------------------------------------------------------------- /pyexcel_ods3/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_ods3 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | The lower level ods file format handler using ezodf 6 | 7 | :copyright: (c) 2015-2017 by Onni Software Ltd & its contributors 8 | :license: New BSD License 9 | """ 10 | 11 | # flake8: noqa 12 | from pyexcel_io.io import get_data as read_data 13 | from pyexcel_io.io import isstream 14 | from pyexcel_io.io import store_data as write_data 15 | 16 | # this line has to be place above all else 17 | # because of dynamic import 18 | from pyexcel_io.plugins import IOPluginInfoChain, IOPluginInfoChainV2 19 | 20 | __FILE_TYPE__ = "ods" 21 | IOPluginInfoChainV2(__name__).add_a_reader( 22 | relative_plugin_class_path="odsr.ODSBook", 23 | locations=["file", "memory"], 24 | file_types=[__FILE_TYPE__], 25 | stream_type="binary", 26 | ).add_a_reader( 27 | relative_plugin_class_path="odsr.ODSBookInContent", 28 | locations=["content"], 29 | file_types=[__FILE_TYPE__], 30 | stream_type="binary", 31 | ).add_a_writer( 32 | relative_plugin_class_path="odsw.ODSWriter", 33 | locations=["file", "memory"], 34 | file_types=[__FILE_TYPE__], 35 | stream_type="binary", 36 | ) 37 | 38 | 39 | def save_data(afile, data, file_type=None, **keywords): 40 | """standalone module function for writing module supported file type""" 41 | if isstream(afile) and file_type is None: 42 | file_type = __FILE_TYPE__ 43 | write_data(afile, data, file_type=file_type, **keywords) 44 | 45 | 46 | def get_data(afile, file_type=None, **keywords): 47 | """standalone module function for reading module supported file type""" 48 | if isstream(afile) and file_type is None: 49 | file_type = __FILE_TYPE__ 50 | return read_data(afile, file_type=file_type, **keywords) 51 | -------------------------------------------------------------------------------- /pyexcel_ods3/odsr.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_ods3.odsr 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | ods reader 6 | 7 | :copyright: (c) 2015-2022 by Onni Software Ltd. & its contributors 8 | :license: New BSD License 9 | """ 10 | from io import BytesIO 11 | 12 | import ezodf 13 | import pyexcel_io.service as service 14 | from pyexcel_io.plugin_api import ISheet, IReader, NamedContent 15 | 16 | 17 | class ODSSheet(ISheet): 18 | """ODS sheet representation""" 19 | 20 | def __init__(self, sheet, auto_detect_int=True): 21 | self.auto_detect_int = auto_detect_int 22 | self.ods_sheet = sheet 23 | 24 | def row_iterator(self): 25 | """ 26 | Number of rows in the xls sheet 27 | """ 28 | return range(self.ods_sheet.nrows()) 29 | 30 | def column_iterator(self, row): 31 | """ 32 | Number of columns in the xls sheet 33 | """ 34 | for column in range(self.ods_sheet.ncols()): 35 | yield self.cell_value(row, column) 36 | 37 | def cell_value(self, row, column): 38 | cell = self.ods_sheet.get_cell((row, column)) 39 | cell_type = cell.value_type 40 | ret = None 41 | if cell_type == "currency": 42 | cell_value = cell.value 43 | if service.has_no_digits_in_float(cell_value): 44 | cell_value = int(cell_value) 45 | 46 | if cell.currency is None: 47 | ret = str(cell_value) 48 | else: 49 | ret = str(cell_value) + " " + cell.currency 50 | elif cell_type in service.ODS_FORMAT_CONVERSION: 51 | value = cell.value 52 | n_value = service.VALUE_CONVERTERS[cell_type](value) 53 | if cell_type == "float" and self.auto_detect_int: 54 | if service.has_no_digits_in_float(n_value): 55 | n_value = int(n_value) 56 | ret = n_value 57 | else: 58 | if cell.value is None: 59 | ret = "" 60 | else: 61 | ret = cell.value 62 | return ret 63 | 64 | 65 | class ODSBook(IReader): 66 | def __init__(self, file_alike_object, file_type, **keywords): 67 | self.ods_book = ezodf.opendoc(file_alike_object) 68 | self._keywords = keywords 69 | self.content_array = [ 70 | NamedContent(sheet.name, sheet) for sheet in self.ods_book.sheets 71 | ] 72 | 73 | def read_sheet(self, native_sheet_index): 74 | native_sheet = self.content_array[native_sheet_index].payload 75 | sheet = ODSSheet(native_sheet, **self._keywords) 76 | return sheet 77 | 78 | def close(self): 79 | self.ods_book = None 80 | 81 | 82 | class ODSBookInContent(ODSBook): 83 | """ 84 | Open xlsx as read only mode 85 | """ 86 | 87 | def __init__(self, file_content, file_type, **keywords): 88 | io = BytesIO(file_content) 89 | super().__init__(io, file_type, **keywords) 90 | -------------------------------------------------------------------------------- /pyexcel_ods3/odsw.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyexcel_ods3.odsw 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | ods writer using ezodf 6 | 7 | :copyright: (c) 2015-2022 by Onni Software Ltd. & its contributors 8 | :license: New BSD License 9 | """ 10 | import types 11 | 12 | import ezodf 13 | import pyexcel_io.service as service 14 | from pyexcel_io.constants import MAX_INTEGER 15 | from pyexcel_io.exceptions import IntegerAccuracyLossError 16 | from pyexcel_io.plugin_api import IWriter, ISheetWriter 17 | 18 | 19 | class ODSSheetWriter(ISheetWriter): 20 | """ 21 | ODS sheet writer 22 | """ 23 | 24 | def __init__(self, ods_book, ods_sheet, sheet_name, **keywords): 25 | self.ods_book = ods_book 26 | self.ods_sheet = ezodf.Sheet(sheet_name) 27 | self.current_row = 0 28 | 29 | def _set_size(self, size): 30 | self.ods_sheet.reset(size=size) 31 | 32 | def write_row(self, array): 33 | """ 34 | write a row into the file 35 | """ 36 | count = 0 37 | for cell in array: 38 | value_type = service.ODS_WRITE_FORMAT_COVERSION[type(cell)] 39 | if value_type == "time": 40 | cell = cell.strftime("PT%HH%MM%SS") 41 | elif value_type == "timedelta": 42 | hours = cell.days * 24 + cell.seconds // 3600 43 | minutes = (cell.seconds // 60) % 60 44 | seconds = cell.seconds % 60 45 | cell = "PT%02dH%02dM%02dS" % (hours, minutes, seconds) 46 | value_type = "time" 47 | if value_type == "datetime": 48 | cell = "%04d-%02d-%02dT%02d:%02d:%02d" % ( 49 | cell.year, 50 | cell.month, 51 | cell.day, 52 | cell.hour, 53 | cell.minute, 54 | cell.second, 55 | ) 56 | value_type = "date" 57 | elif value_type == "float": 58 | if cell > MAX_INTEGER: 59 | raise IntegerAccuracyLossError("%s is too big" % cell) 60 | self.ods_sheet[self.current_row, count].set_value( 61 | cell, value_type=value_type 62 | ) 63 | count += 1 64 | self.current_row += 1 65 | 66 | def write_array(self, table): 67 | to_write_data = table 68 | if isinstance(to_write_data, types.GeneratorType): 69 | to_write_data = list(table) 70 | rows = len(to_write_data) 71 | if rows < 1: 72 | return 73 | columns = max([len(row) for row in to_write_data]) 74 | self._set_size((rows, columns)) 75 | for row in to_write_data: 76 | self.write_row(row) 77 | 78 | def close(self): 79 | """ 80 | This call writes file 81 | 82 | """ 83 | self.ods_book.sheets += self.ods_sheet 84 | 85 | 86 | class ODSWriter(IWriter): 87 | """ 88 | open document spreadsheet writer 89 | 90 | """ 91 | 92 | def __init__( 93 | self, file_alike_object, file_type, skip_backup=True, **keywords 94 | ): 95 | """open a file for writing ods""" 96 | self.ods_book = ezodf.newdoc( 97 | doctype=file_type, filename=file_alike_object 98 | ) 99 | 100 | if skip_backup: 101 | self.ods_book.backup = False 102 | 103 | def create_sheet(self, name): 104 | """ 105 | write a row into the file 106 | """ 107 | return ODSSheetWriter(self.ods_book, None, name) 108 | 109 | def close(self): 110 | """ 111 | This call writes file 112 | 113 | """ 114 | self.ods_book.save() 115 | self.ods_book = None 116 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyexcel-io>=0.6.2 2 | lxml 3 | pyexcel-ezodf>=0.3.3 4 | -------------------------------------------------------------------------------- /rnd_requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-ods3/b2e227a8a4f4df34a498e280883633e5dcc85396/rnd_requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Template by pypi-mobans 5 | """ 6 | 7 | import os 8 | import sys 9 | import codecs 10 | import locale 11 | import platform 12 | from shutil import rmtree 13 | 14 | from setuptools import Command, setup, find_packages 15 | 16 | PY2 = sys.version_info[0] == 2 17 | PY26 = PY2 and sys.version_info[1] < 7 18 | PY33 = sys.version_info < (3, 4) 19 | 20 | # Work around mbcs bug in distutils. 21 | # http://bugs.python.org/issue10945 22 | # This work around is only if a project supports Python < 3.4 23 | 24 | # Work around for locale not being set 25 | try: 26 | lc = locale.getlocale() 27 | pf = platform.system() 28 | if pf != "Windows" and lc == (None, None): 29 | locale.setlocale(locale.LC_ALL, "C.UTF-8") 30 | except (ValueError, UnicodeError, locale.Error): 31 | locale.setlocale(locale.LC_ALL, "en_US.UTF-8") 32 | 33 | NAME = "pyexcel-ods3" 34 | AUTHOR = "C.W." 35 | VERSION = "0.6.1" 36 | EMAIL = "info@pyexcel.org" 37 | LICENSE = "New BSD" 38 | DESCRIPTION = ( 39 | "A wrapper library to read, manipulate and write data in ods format" 40 | ) 41 | URL = "https://github.com/pyexcel/pyexcel-ods3" 42 | DOWNLOAD_URL = "%s/archive/0.6.1.tar.gz" % URL 43 | FILES = ["README.rst", "CONTRIBUTORS.rst", "CHANGELOG.rst"] 44 | KEYWORDS = [ 45 | "python", 46 | 'ods' 47 | ] 48 | 49 | CLASSIFIERS = [ 50 | "Topic :: Software Development :: Libraries", 51 | "Programming Language :: Python", 52 | "Intended Audience :: Developers", 53 | 54 | "Programming Language :: Python :: 3 :: Only", 55 | 56 | 57 | 58 | "Programming Language :: Python :: 3.6", 59 | "Programming Language :: Python :: 3.7", 60 | "Programming Language :: Python :: 3.8", 61 | 62 | 'License :: OSI Approved :: BSD License', 63 | 64 | ] 65 | 66 | PYTHON_REQUIRES = ">=3.6" 67 | 68 | INSTALL_REQUIRES = [ 69 | "pyexcel-io>=0.6.2", 70 | "lxml", 71 | "pyexcel-ezodf>=0.3.3", 72 | ] 73 | SETUP_COMMANDS = {} 74 | 75 | PACKAGES = find_packages(exclude=["ez_setup", "examples", "tests", "tests.*"]) 76 | EXTRAS_REQUIRE = { 77 | } 78 | # You do not need to read beyond this line 79 | PUBLISH_COMMAND = "{0} setup.py sdist bdist_wheel upload -r pypi".format(sys.executable) 80 | HERE = os.path.abspath(os.path.dirname(__file__)) 81 | 82 | GS_COMMAND = ("gease pyexcel-ods3 v0.6.1 " + 83 | "Find 0.6.1 in changelog for more details") 84 | NO_GS_MESSAGE = ("Automatic github release is disabled. " + 85 | "Please install gease to enable it.") 86 | UPLOAD_FAILED_MSG = ( 87 | 'Upload failed. please run "%s" yourself.' % PUBLISH_COMMAND) 88 | 89 | 90 | class PublishCommand(Command): 91 | """Support setup.py upload.""" 92 | 93 | description = "Build and publish the package on github and pypi" 94 | user_options = [] 95 | 96 | @staticmethod 97 | def status(s): 98 | """Prints things in bold.""" 99 | print("\033[1m{0}\033[0m".format(s)) 100 | 101 | def initialize_options(self): 102 | pass 103 | 104 | def finalize_options(self): 105 | pass 106 | 107 | def run(self): 108 | try: 109 | self.status("Removing previous builds...") 110 | rmtree(os.path.join(HERE, "dist")) 111 | rmtree(os.path.join(HERE, "build")) 112 | rmtree(os.path.join(HERE, "pyexcel_ods3.egg-info")) 113 | except OSError: 114 | pass 115 | 116 | self.status("Building Source and Wheel (universal) distribution...") 117 | run_status = True 118 | if has_gease(): 119 | run_status = os.system(GS_COMMAND) == 0 120 | else: 121 | self.status(NO_GS_MESSAGE) 122 | if run_status: 123 | if os.system(PUBLISH_COMMAND) != 0: 124 | self.status(UPLOAD_FAILED_MSG) 125 | 126 | sys.exit() 127 | 128 | 129 | SETUP_COMMANDS.update({ 130 | "publish": PublishCommand 131 | }) 132 | 133 | def has_gease(): 134 | """ 135 | test if github release command is installed 136 | 137 | visit http://github.com/moremoban/gease for more info 138 | """ 139 | try: 140 | import gease # noqa 141 | return True 142 | except ImportError: 143 | return False 144 | 145 | 146 | def read_files(*files): 147 | """Read files into setup""" 148 | text = "" 149 | for single_file in files: 150 | content = read(single_file) 151 | text = text + content + "\n" 152 | return text 153 | 154 | 155 | def read(afile): 156 | """Read a file into setup""" 157 | the_relative_file = os.path.join(HERE, afile) 158 | with codecs.open(the_relative_file, "r", "utf-8") as opened_file: 159 | content = filter_out_test_code(opened_file) 160 | content = "".join(list(content)) 161 | return content 162 | 163 | 164 | def filter_out_test_code(file_handle): 165 | found_test_code = False 166 | for line in file_handle.readlines(): 167 | if line.startswith(".. testcode:"): 168 | found_test_code = True 169 | continue 170 | if found_test_code is True: 171 | if line.startswith(" "): 172 | continue 173 | else: 174 | empty_line = line.strip() 175 | if len(empty_line) == 0: 176 | continue 177 | else: 178 | found_test_code = False 179 | yield line 180 | else: 181 | for keyword in ["|version|", "|today|"]: 182 | if keyword in line: 183 | break 184 | else: 185 | yield line 186 | 187 | 188 | if __name__ == "__main__": 189 | setup( 190 | test_suite="tests", 191 | name=NAME, 192 | author=AUTHOR, 193 | version=VERSION, 194 | author_email=EMAIL, 195 | description=DESCRIPTION, 196 | url=URL, 197 | download_url=DOWNLOAD_URL, 198 | long_description=read_files(*FILES), 199 | license=LICENSE, 200 | keywords=KEYWORDS, 201 | python_requires=PYTHON_REQUIRES, 202 | extras_require=EXTRAS_REQUIRE, 203 | tests_require=["nose"], 204 | install_requires=INSTALL_REQUIRES, 205 | packages=PACKAGES, 206 | include_package_data=True, 207 | zip_safe=False, 208 | classifiers=CLASSIFIERS, 209 | cmdclass=SETUP_COMMANDS 210 | ) 211 | -------------------------------------------------------------------------------- /test.bat: -------------------------------------------------------------------------------- 1 | pip freeze 2 | nosetests --with-coverage --cover-package pyexcel_ods3 --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_ods3 3 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | pip freeze 3 | nosetests --with-coverage --cover-package pyexcel_ods3 --cover-package tests tests --with-doctest --doctest-extension=.rst README.rst docs/source pyexcel_ods3 4 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import os # noqa 2 | import datetime # noqa 3 | 4 | import pyexcel 5 | 6 | from nose.tools import eq_, raises # noqa 7 | 8 | 9 | def create_sample_file1(file): 10 | data = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1] 11 | table = [] 12 | table.append(data[:4]) 13 | table.append(data[4:8]) 14 | table.append(data[8:12]) 15 | pyexcel.save_as(array=table, dest_file_name=file) 16 | 17 | 18 | class PyexcelHatWriterBase: 19 | """ 20 | Abstract functional test for hat writers 21 | """ 22 | 23 | content = { 24 | "X": [1, 2, 3, 4, 5], 25 | "Y": [6, 7, 8, 9, 10], 26 | "Z": [11, 12, 13, 14, 15], 27 | } 28 | 29 | def test_series_table(self): 30 | pyexcel.save_as(adict=self.content, dest_file_name=self.testfile) 31 | r = pyexcel.get_sheet(file_name=self.testfile, name_columns_by_row=0) 32 | eq_(r.dict, self.content) 33 | 34 | 35 | class PyexcelWriterBase: 36 | """ 37 | Abstract functional test for writers 38 | 39 | testfile and testfile2 have to be initialized before 40 | it is used for testing 41 | """ 42 | 43 | content = [ 44 | [1, 2, 3, 4, 5], 45 | [1, 2, 3, 4, 5], 46 | [1, 2, 3, 4, 5], 47 | [1, 2, 3, 4, 5], 48 | ] 49 | 50 | def _create_a_file(self, file): 51 | pyexcel.save_as(dest_file_name=file, array=self.content) 52 | 53 | def test_write_array(self): 54 | self._create_a_file(self.testfile) 55 | r = pyexcel.get_sheet(file_name=self.testfile) 56 | actual = list(r.rows()) 57 | assert actual == self.content 58 | 59 | 60 | class PyexcelMultipleSheetBase: 61 | def _write_test_file(self, filename): 62 | pyexcel.save_book_as(bookdict=self.content, dest_file_name=filename) 63 | 64 | def _clean_up(self): 65 | if os.path.exists(self.testfile2): 66 | os.unlink(self.testfile2) 67 | if os.path.exists(self.testfile): 68 | os.unlink(self.testfile) 69 | 70 | def test_sheet_names(self): 71 | r = pyexcel.BookReader(self.testfile) 72 | expected = ["Sheet1", "Sheet2", "Sheet3"] 73 | sheet_names = r.sheet_names() 74 | for name in sheet_names: 75 | assert name in expected 76 | 77 | def test_reading_through_sheets(self): 78 | b = pyexcel.BookReader(self.testfile) 79 | data = list(b["Sheet1"].rows()) 80 | expected = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]] 81 | assert data == expected 82 | data = list(b["Sheet2"].rows()) 83 | expected = [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]] 84 | assert data == expected 85 | data = list(b["Sheet3"].rows()) 86 | expected = [[u"X", u"Y", u"Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]] 87 | assert data == expected 88 | sheet3 = b["Sheet3"] 89 | sheet3.name_columns_by_row(0) 90 | data = list(b["Sheet3"].rows()) 91 | expected = [[1, 4, 7], [2, 5, 8], [3, 6, 9]] 92 | assert data == expected 93 | 94 | 95 | class ODSCellTypes: 96 | def test_formats(self): 97 | # date formats 98 | date_format = "%d/%m/%Y" 99 | eq_(self.data["Sheet1"][0][0], "Date") 100 | eq_(self.data["Sheet1"][1][0].strftime(date_format), "11/11/2014") 101 | eq_(self.data["Sheet1"][2][0].strftime(date_format), "01/01/2001") 102 | eq_(self.data["Sheet1"][3][0], "") 103 | # time formats 104 | time_format = "%S:%M:%H" 105 | eq_(self.data["Sheet1"][0][1], "Time") 106 | eq_(self.data["Sheet1"][1][1].strftime(time_format), "12:12:11") 107 | eq_(self.data["Sheet1"][2][1].strftime(time_format), "12:00:00") 108 | eq_(self.data["Sheet1"][3][1], 0) 109 | eq_( 110 | self.data["Sheet1"][4][1], 111 | datetime.timedelta(hours=27, minutes=17, seconds=54), 112 | ) 113 | eq_(self.data["Sheet1"][5][1], "Other") 114 | # boolean 115 | eq_(self.data["Sheet1"][0][2], "Boolean") 116 | eq_(self.data["Sheet1"][1][2], True) 117 | eq_(self.data["Sheet1"][2][2], False) 118 | # Float 119 | eq_(self.data["Sheet1"][0][3], "Float") 120 | eq_(self.data["Sheet1"][1][3], 11.11) 121 | # Currency 122 | eq_(self.data["Sheet1"][0][4], "Currency") 123 | eq_(self.data["Sheet1"][1][4], "1 GBP") 124 | eq_(self.data["Sheet1"][2][4], "-10000 GBP") 125 | # Percentage 126 | eq_(self.data["Sheet1"][0][5], "Percentage") 127 | eq_(self.data["Sheet1"][1][5], 2) 128 | # int 129 | eq_(self.data["Sheet1"][0][6], "Int") 130 | eq_(self.data["Sheet1"][1][6], 3) 131 | eq_(self.data["Sheet1"][4][6], 11) 132 | # Scientifed not supported 133 | eq_(self.data["Sheet1"][1][7], 100000) 134 | # Fraction 135 | eq_(self.data["Sheet1"][1][8], 1.25) 136 | # Text 137 | eq_(self.data["Sheet1"][1][9], "abc") 138 | 139 | @raises(IndexError) 140 | def test_no_excessive_trailing_columns(self): 141 | eq_(self.data["Sheet1"][2][6], "") 142 | -------------------------------------------------------------------------------- /tests/fixtures/12_day_as_time.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-ods3/b2e227a8a4f4df34a498e280883633e5dcc85396/tests/fixtures/12_day_as_time.ods -------------------------------------------------------------------------------- /tests/fixtures/currency_without_currency.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-ods3/b2e227a8a4f4df34a498e280883633e5dcc85396/tests/fixtures/currency_without_currency.ods -------------------------------------------------------------------------------- /tests/fixtures/file_with_an_empty_sheet.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-ods3/b2e227a8a4f4df34a498e280883633e5dcc85396/tests/fixtures/file_with_an_empty_sheet.ods -------------------------------------------------------------------------------- /tests/fixtures/multilineods.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-ods3/b2e227a8a4f4df34a498e280883633e5dcc85396/tests/fixtures/multilineods.ods -------------------------------------------------------------------------------- /tests/fixtures/ods_formats.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyexcel/pyexcel-ods3/b2e227a8a4f4df34a498e280883633e5dcc85396/tests/fixtures/ods_formats.ods -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | mock;python_version<"3" 3 | codecov 4 | coverage 5 | flake8 6 | black 7 | isort 8 | collective.checkdocs 9 | pygments 10 | moban 11 | moban_jinja2_github 12 | pyexcel 13 | psutil 14 | pyexcel-xls 15 | -------------------------------------------------------------------------------- /tests/test_bug_fixes.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import psutil 4 | import pyexcel as pe 5 | from pyexcel_io.exceptions import IntegerAccuracyLossError 6 | 7 | from nose import SkipTest 8 | from nose.tools import eq_, raises 9 | 10 | IN_TRAVIS = "TRAVIS" in os.environ 11 | 12 | 13 | def test_issue_10(): 14 | test_file_name = "test_issue_10.ods" 15 | from pyexcel_ods3 import save_data 16 | 17 | content = {"test": [[1, 2]]} 18 | save_data(test_file_name, content) 19 | save_data(test_file_name, content) 20 | assert os.path.exists(test_file_name) 21 | assert os.path.exists(test_file_name + ".bak") is False 22 | os.unlink(test_file_name) 23 | 24 | 25 | row_max = 2 26 | col_max = 2 27 | 28 | 29 | def data_gen(): 30 | for row in range(row_max // 2): 31 | tmp = [] 32 | for col in range(col_max): 33 | tmp.append("Row: %d Col: %d" % (row, col)) 34 | for col in range(col_max): 35 | tmp.append((row + col)) 36 | yield tmp 37 | 38 | 39 | def test_issue_11(): 40 | test_file = "test_file.ods" 41 | from pyexcel_ods3 import save_data 42 | 43 | save_data(test_file, {"generator": data_gen()}) 44 | os.unlink(test_file) 45 | 46 | 47 | def test_issue_8(): 48 | from pyexcel_ods3 import get_data 49 | 50 | test_file = "12_day_as_time.ods" 51 | data = get_data(get_fixtures(test_file), skip_empty_rows=True) 52 | eq_(data["Sheet1"][0][0].days, 12) 53 | 54 | 55 | def test_issue_83_ods_file_handle(): 56 | # this proves that odfpy 57 | # does not leave a file handle open at all 58 | proc = psutil.Process() 59 | test_file = get_fixtures("12_day_as_time.ods") 60 | open_files_l1 = proc.open_files() 61 | 62 | # start with a csv file 63 | data = pe.iget_array(file_name=test_file, library="pyexcel-ods3") 64 | open_files_l2 = proc.open_files() 65 | delta = len(open_files_l2) - len(open_files_l1) 66 | # cannot catch open file handle 67 | assert delta == 0 68 | 69 | # now the file handle get opened when we run through 70 | # the generator 71 | list(data) 72 | open_files_l3 = proc.open_files() 73 | delta = len(open_files_l3) - len(open_files_l1) 74 | # cannot catch open file handle 75 | assert delta == 0 76 | 77 | # free the fish 78 | pe.free_resources() 79 | open_files_l4 = proc.open_files() 80 | # this confirms that no more open file handle 81 | eq_(open_files_l1, open_files_l4) 82 | 83 | 84 | def test_issue_23(): 85 | if not IN_TRAVIS: 86 | raise SkipTest() 87 | url = ( 88 | "https://github.com/pyexcel/pyexcel-ods3/" 89 | + "raw/master/tests/fixtures/multilineods.ods" 90 | ) 91 | pe.get_book(url=url) 92 | 93 | 94 | def test_issue_30(): 95 | test_file = "issue_30.ods" 96 | sheet = pe.Sheet() 97 | sheet[0, 0] = 999999999999999 98 | sheet.save_as(test_file) 99 | sheet2 = pe.get_sheet(file_name=test_file) 100 | eq_(sheet[0, 0], sheet2[0, 0]) 101 | os.unlink(test_file) 102 | 103 | 104 | @raises(IntegerAccuracyLossError) 105 | def test_issue_30_precision_loss(): 106 | test_file = "issue_30_2.ods" 107 | sheet = pe.Sheet() 108 | sheet[0, 0] = 9999999999999999 109 | sheet.save_as(test_file) 110 | 111 | 112 | def test_issue_36(): 113 | from pyexcel_ods3 import get_data 114 | 115 | test_file = "currency_without_currency.ods" 116 | data = get_data(get_fixtures(test_file)) 117 | eq_(data["Sheet1"][6][0], '1.75"') 118 | eq_(data["Sheet1"][6][5], "95") 119 | eq_(data["Sheet1"][6][6], 25) 120 | 121 | 122 | def get_fixtures(filename): 123 | return os.path.join("tests", "fixtures", filename) 124 | -------------------------------------------------------------------------------- /tests/test_filter.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyexcel_io import get_data, save_data 4 | 5 | from nose.tools import eq_ 6 | 7 | 8 | class TestFilter: 9 | def setUp(self): 10 | self.test_file = "test_filter.ods" 11 | sample = [ 12 | [1, 21, 31], 13 | [2, 22, 32], 14 | [3, 23, 33], 15 | [4, 24, 34], 16 | [5, 25, 35], 17 | [6, 26, 36], 18 | ] 19 | save_data(self.test_file, sample) 20 | self.sheet_name = "pyexcel_sheet1" 21 | 22 | def test_filter_row(self): 23 | filtered_data = get_data( 24 | self.test_file, start_row=3, library="pyexcel-ods3" 25 | ) 26 | expected = [[4, 24, 34], [5, 25, 35], [6, 26, 36]] 27 | eq_(filtered_data[self.sheet_name], expected) 28 | 29 | def test_filter_row_2(self): 30 | filtered_data = get_data( 31 | self.test_file, start_row=3, row_limit=1, library="pyexcel-ods3" 32 | ) 33 | expected = [[4, 24, 34]] 34 | eq_(filtered_data[self.sheet_name], expected) 35 | 36 | def test_filter_column(self): 37 | filtered_data = get_data( 38 | self.test_file, start_column=1, library="pyexcel-ods3" 39 | ) 40 | expected = [[21, 31], [22, 32], [23, 33], [24, 34], [25, 35], [26, 36]] 41 | eq_(filtered_data[self.sheet_name], expected) 42 | 43 | def test_filter_column_2(self): 44 | filtered_data = get_data( 45 | self.test_file, 46 | start_column=1, 47 | column_limit=1, 48 | library="pyexcel-ods3", 49 | ) 50 | expected = [[21], [22], [23], [24], [25], [26]] 51 | eq_(filtered_data[self.sheet_name], expected) 52 | 53 | def test_filter_both_ways(self): 54 | filtered_data = get_data( 55 | self.test_file, start_column=1, start_row=3, library="pyexcel-ods3" 56 | ) 57 | expected = [[24, 34], [25, 35], [26, 36]] 58 | eq_(filtered_data[self.sheet_name], expected) 59 | 60 | def test_filter_both_ways_2(self): 61 | filtered_data = get_data( 62 | self.test_file, 63 | start_column=1, 64 | column_limit=1, 65 | start_row=3, 66 | row_limit=1, 67 | library="pyexcel-ods3", 68 | ) 69 | expected = [[24]] 70 | eq_(filtered_data[self.sheet_name], expected) 71 | 72 | def tearDown(self): 73 | os.unlink(self.test_file) 74 | -------------------------------------------------------------------------------- /tests/test_formatters.py: -------------------------------------------------------------------------------- 1 | import os 2 | from textwrap import dedent 3 | 4 | import pyexcel as pe 5 | 6 | from nose.tools import eq_ 7 | 8 | 9 | class TestAutoDetectInt: 10 | def setUp(self): 11 | self.content = [[1, 2, 3.1]] 12 | self.test_file = "test_auto_detect_init.ods" 13 | pe.save_as(array=self.content, dest_file_name=self.test_file) 14 | 15 | def test_auto_detect_int(self): 16 | sheet = pe.get_sheet(file_name=self.test_file, library="pyexcel-ods3") 17 | expected = dedent( 18 | """ 19 | pyexcel_sheet1: 20 | +---+---+-----+ 21 | | 1 | 2 | 3.1 | 22 | +---+---+-----+""" 23 | ).strip() 24 | eq_(str(sheet), expected) 25 | 26 | def test_get_book_auto_detect_int(self): 27 | book = pe.get_book(file_name=self.test_file, library="pyexcel-ods3") 28 | expected = dedent( 29 | """ 30 | pyexcel_sheet1: 31 | +---+---+-----+ 32 | | 1 | 2 | 3.1 | 33 | +---+---+-----+""" 34 | ).strip() 35 | eq_(str(book), expected) 36 | 37 | def test_auto_detect_int_false(self): 38 | sheet = pe.get_sheet( 39 | file_name=self.test_file, 40 | auto_detect_int=False, 41 | library="pyexcel-ods3", 42 | ) 43 | expected = dedent( 44 | """ 45 | pyexcel_sheet1: 46 | +-----+-----+-----+ 47 | | 1.0 | 2.0 | 3.1 | 48 | +-----+-----+-----+""" 49 | ).strip() 50 | eq_(str(sheet), expected) 51 | 52 | def test_get_book_auto_detect_int_false(self): 53 | book = pe.get_book( 54 | file_name=self.test_file, 55 | auto_detect_int=False, 56 | library="pyexcel-ods3", 57 | ) 58 | expected = dedent( 59 | """ 60 | pyexcel_sheet1: 61 | +-----+-----+-----+ 62 | | 1.0 | 2.0 | 3.1 | 63 | +-----+-----+-----+""" 64 | ).strip() 65 | eq_(str(book), expected) 66 | 67 | def tearDown(self): 68 | os.unlink(self.test_file) 69 | -------------------------------------------------------------------------------- /tests/test_multiline_feature.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyexcel 4 | 5 | 6 | def test_reading_multiline_ods(): 7 | testfile = os.path.join("tests", "fixtures", "multilineods.ods") 8 | sheet = pyexcel.get_sheet(file_name=testfile) 9 | assert sheet[0, 0] == "1\n2\n3\n4" 10 | assert sheet[1, 0] == "Line 1\n\nLine 2" 11 | 12 | 13 | def test_writing_multiline_ods(): 14 | content = "2\n3\n4\n993939\n\na" 15 | testfile = "writemultiline.ods" 16 | array = [[content, "test"]] 17 | pyexcel.save_as(array=array, dest_file_name=testfile) 18 | sheet = pyexcel.get_sheet(file_name=testfile) 19 | assert sheet[0, 0] == content 20 | os.unlink(testfile) 21 | -------------------------------------------------------------------------------- /tests/test_multiple_sheets.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import OrderedDict 3 | 4 | import pyexcel 5 | from base import PyexcelMultipleSheetBase 6 | 7 | from nose.tools import raises 8 | 9 | 10 | class TestOdsNxlsMultipleSheets(PyexcelMultipleSheetBase): 11 | def setUp(self): 12 | self.testfile = "multiple1.ods" 13 | self.testfile2 = "multiple1.xls" 14 | self.content = _produce_ordered_dict() 15 | self._write_test_file(self.testfile) 16 | 17 | def tearDown(self): 18 | self._clean_up() 19 | 20 | 21 | class TestXlsNOdsMultipleSheets(PyexcelMultipleSheetBase): 22 | def setUp(self): 23 | self.testfile = "multiple1.xls" 24 | self.testfile2 = "multiple1.ods" 25 | self.content = _produce_ordered_dict() 26 | self._write_test_file(self.testfile) 27 | 28 | def tearDown(self): 29 | self._clean_up() 30 | 31 | 32 | class TestAddBooks: 33 | def _write_test_file(self, file): 34 | """ 35 | Make a test file as: 36 | 37 | 1,1,1,1 38 | 2,2,2,2 39 | 3,3,3,3 40 | """ 41 | self.rows = 3 42 | pyexcel.save_book_as(bookdict=self.content, dest_file_name=file) 43 | 44 | def setUp(self): 45 | self.testfile = "multiple1.ods" 46 | self.testfile2 = "multiple1.xls" 47 | self.content = _produce_ordered_dict() 48 | self._write_test_file(self.testfile) 49 | self._write_test_file(self.testfile2) 50 | 51 | def test_load_a_single_sheet(self): 52 | b1 = pyexcel.get_book(file_name=self.testfile, sheet_name="Sheet1") 53 | assert len(b1.sheet_names()) == 1 54 | assert b1["Sheet1"].to_array() == self.content["Sheet1"] 55 | 56 | def test_load_a_single_sheet2(self): 57 | b1 = pyexcel.load_book(self.testfile, sheet_index=0) 58 | assert len(b1.sheet_names()) == 1 59 | assert b1["Sheet1"].to_array() == self.content["Sheet1"] 60 | 61 | @raises(IndexError) 62 | def test_load_a_single_sheet3(self): 63 | pyexcel.get_book(file_name=self.testfile, sheet_index=10000) 64 | 65 | @raises(ValueError) 66 | def test_load_a_single_sheet4(self): 67 | pyexcel.get_book(file_name=self.testfile, sheet_name="Not exist") 68 | 69 | def test_delete_sheets(self): 70 | b1 = pyexcel.load_book(self.testfile) 71 | assert len(b1.sheet_names()) == 3 72 | del b1["Sheet1"] 73 | assert len(b1.sheet_names()) == 2 74 | try: 75 | del b1["Sheet1"] 76 | assert 1 == 2 77 | except KeyError: 78 | assert 1 == 1 79 | del b1[1] 80 | assert len(b1.sheet_names()) == 1 81 | try: 82 | del b1[1] 83 | assert 1 == 2 84 | except IndexError: 85 | assert 1 == 1 86 | 87 | def test_delete_sheets2(self): 88 | """repetitively delete first sheet""" 89 | b1 = pyexcel.load_book(self.testfile) 90 | del b1[0] 91 | assert len(b1.sheet_names()) == 2 92 | del b1[0] 93 | assert len(b1.sheet_names()) == 1 94 | del b1[0] 95 | assert len(b1.sheet_names()) == 0 96 | 97 | def test_add_book1(self): 98 | """ 99 | test this scenario: book3 = book1 + book2 100 | """ 101 | b1 = pyexcel.get_book(file_name=self.testfile) 102 | b2 = pyexcel.get_book(file_name=self.testfile2) 103 | b3 = b1 + b2 104 | content = b3.dict 105 | sheet_names = content.keys() 106 | assert len(sheet_names) == 6 107 | for name in sheet_names: 108 | if "Sheet3" in name: 109 | assert content[name] == self.content["Sheet3"] 110 | elif "Sheet2" in name: 111 | assert content[name] == self.content["Sheet2"] 112 | elif "Sheet1" in name: 113 | assert content[name] == self.content["Sheet1"] 114 | 115 | def test_add_book1_in_place(self): 116 | """ 117 | test this scenario: book1 += book2 118 | """ 119 | b1 = pyexcel.BookReader(self.testfile) 120 | b2 = pyexcel.BookReader(self.testfile2) 121 | b1 += b2 122 | content = b1.dict 123 | sheet_names = content.keys() 124 | assert len(sheet_names) == 6 125 | for name in sheet_names: 126 | if "Sheet3" in name: 127 | assert content[name] == self.content["Sheet3"] 128 | elif "Sheet2" in name: 129 | assert content[name] == self.content["Sheet2"] 130 | elif "Sheet1" in name: 131 | assert content[name] == self.content["Sheet1"] 132 | 133 | def test_add_book2(self): 134 | """ 135 | test this scenario: book3 = book1 + sheet3 136 | """ 137 | b1 = pyexcel.BookReader(self.testfile) 138 | b2 = pyexcel.BookReader(self.testfile2) 139 | b3 = b1 + b2["Sheet3"] 140 | content = b3.dict 141 | sheet_names = content.keys() 142 | assert len(sheet_names) == 4 143 | for name in sheet_names: 144 | if "Sheet3" in name: 145 | assert content[name] == self.content["Sheet3"] 146 | elif "Sheet2" in name: 147 | assert content[name] == self.content["Sheet2"] 148 | elif "Sheet1" in name: 149 | assert content[name] == self.content["Sheet1"] 150 | 151 | def test_add_book2_in_place(self): 152 | """ 153 | test this scenario: book3 = book1 + sheet3 154 | """ 155 | b1 = pyexcel.BookReader(self.testfile) 156 | b2 = pyexcel.BookReader(self.testfile2) 157 | b1 += b2["Sheet3"] 158 | content = b1.dict 159 | sheet_names = content.keys() 160 | assert len(sheet_names) == 4 161 | for name in sheet_names: 162 | if "Sheet3" in name: 163 | assert content[name] == self.content["Sheet3"] 164 | elif "Sheet2" in name: 165 | assert content[name] == self.content["Sheet2"] 166 | elif "Sheet1" in name: 167 | assert content[name] == self.content["Sheet1"] 168 | 169 | def test_add_book3(self): 170 | """ 171 | test this scenario: book3 = sheet1 + sheet2 172 | """ 173 | b1 = pyexcel.BookReader(self.testfile) 174 | b2 = pyexcel.BookReader(self.testfile2) 175 | b3 = b1["Sheet1"] + b2["Sheet3"] 176 | content = b3.dict 177 | sheet_names = content.keys() 178 | assert len(sheet_names) == 2 179 | assert content["Sheet3"] == self.content["Sheet3"] 180 | assert content["Sheet1"] == self.content["Sheet1"] 181 | 182 | def test_add_book4(self): 183 | """ 184 | test this scenario: book3 = sheet1 + book 185 | """ 186 | b1 = pyexcel.BookReader(self.testfile) 187 | b2 = pyexcel.BookReader(self.testfile2) 188 | b3 = b1["Sheet1"] + b2 189 | content = b3.dict 190 | sheet_names = content.keys() 191 | assert len(sheet_names) == 4 192 | for name in sheet_names: 193 | if "Sheet3" in name: 194 | assert content[name] == self.content["Sheet3"] 195 | elif "Sheet2" in name: 196 | assert content[name] == self.content["Sheet2"] 197 | elif "Sheet1" in name: 198 | assert content[name] == self.content["Sheet1"] 199 | 200 | def test_add_book_error(self): 201 | """ 202 | test this scenario: book3 = sheet1 + book 203 | """ 204 | b1 = pyexcel.BookReader(self.testfile) 205 | try: 206 | b1 + 12 207 | assert 1 == 2 208 | except TypeError: 209 | assert 1 == 1 210 | try: 211 | b1 += 12 212 | assert 1 == 2 213 | except TypeError: 214 | assert 1 == 1 215 | 216 | def tearDown(self): 217 | if os.path.exists(self.testfile): 218 | os.unlink(self.testfile) 219 | if os.path.exists(self.testfile2): 220 | os.unlink(self.testfile2) 221 | 222 | 223 | class TestMultiSheetReader: 224 | def setUp(self): 225 | self.testfile = "file_with_an_empty_sheet.ods" 226 | 227 | def test_reader_with_correct_sheets(self): 228 | r = pyexcel.BookReader( 229 | os.path.join("tests", "fixtures", self.testfile) 230 | ) 231 | assert r.number_of_sheets() == 3 232 | 233 | 234 | def _produce_ordered_dict(): 235 | data_dict = OrderedDict() 236 | data_dict.update({"Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]}) 237 | data_dict.update({"Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]]}) 238 | data_dict.update( 239 | {"Sheet3": [[u"X", u"Y", u"Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]]} 240 | ) 241 | return data_dict 242 | -------------------------------------------------------------------------------- /tests/test_ods_reader.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from base import ODSCellTypes 4 | from pyexcel_io.reader import Reader 5 | from pyexcel_ods3.odsw import ODSWriter 6 | 7 | 8 | class TestODSReader(ODSCellTypes): 9 | def setUp(self): 10 | r = Reader("ods") 11 | r.open(os.path.join("tests", "fixtures", "ods_formats.ods")) 12 | self.data = r.read_all() 13 | for key in self.data.keys(): 14 | self.data[key] = list(self.data[key]) 15 | r.close() 16 | 17 | 18 | class TestODSWriter(ODSCellTypes): 19 | def setUp(self): 20 | r = Reader("ods") 21 | r.open( 22 | os.path.join("tests", "fixtures", "ods_formats.ods"), 23 | skip_empty_rows=True, 24 | ) 25 | self.data1 = r.read_all() 26 | r.close() 27 | self.testfile = "odswriter.ods" 28 | w = ODSWriter(self.testfile, "ods") 29 | w.write(self.data1) 30 | w.close() 31 | r.open(self.testfile) 32 | self.data = r.read_all() 33 | 34 | for key in self.data.keys(): 35 | self.data[key] = list(self.data[key]) 36 | r.close() 37 | 38 | def tearDown(self): 39 | if os.path.exists(self.testfile): 40 | os.unlink(self.testfile) 41 | -------------------------------------------------------------------------------- /tests/test_stringio.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pyexcel 4 | from base import create_sample_file1 5 | 6 | from nose.tools import eq_ 7 | 8 | 9 | class TestStringIO: 10 | def test_ods_stringio(self): 11 | testfile = "cute.ods" 12 | create_sample_file1(testfile) 13 | with open(testfile, "rb") as f: 14 | content = f.read() 15 | r = pyexcel.get_sheet( 16 | file_type="ods", file_content=content, library="pyexcel-ods3" 17 | ) 18 | result = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", 1.1, 1] 19 | actual = list(r.enumerate()) 20 | eq_(result, actual) 21 | if os.path.exists(testfile): 22 | os.unlink(testfile) 23 | 24 | def test_ods_output_stringio(self): 25 | data = [[1, 2, 3], [4, 5, 6]] 26 | io = pyexcel.save_as(dest_file_type="ods", array=data) 27 | r = pyexcel.get_sheet( 28 | file_type="ods", file_content=io.getvalue(), library="pyexcel-ods3" 29 | ) 30 | result = [1, 2, 3, 4, 5, 6] 31 | actual = list(r.enumerate()) 32 | eq_(result, actual) 33 | -------------------------------------------------------------------------------- /tests/test_writer.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from base import PyexcelWriterBase, PyexcelHatWriterBase 4 | from pyexcel_ods3 import get_data 5 | from pyexcel_ods3.odsw import ODSWriter as Writer 6 | 7 | from nose.tools import eq_ 8 | 9 | 10 | class TestNativeODSWriter: 11 | def test_write_book(self): 12 | self.content = { 13 | "Sheet1": [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]], 14 | "Sheet2": [[4, 4, 4, 4], [5, 5, 5, 5], [6, 6, 6, 6]], 15 | "Sheet3": [[u"X", u"Y", u"Z"], [1, 4, 7], [2, 5, 8], [3, 6, 9]], 16 | } 17 | self.testfile = "writer.ods" 18 | writer = Writer(self.testfile, "ods") 19 | writer.write(self.content) 20 | writer.close() 21 | content = get_data(self.testfile) 22 | for key in content.keys(): 23 | content[key] = list(content[key]) 24 | assert content == self.content 25 | 26 | def tearDown(self): 27 | if os.path.exists(self.testfile): 28 | os.unlink(self.testfile) 29 | 30 | 31 | class TestodsnCSVWriter(PyexcelWriterBase): 32 | def setUp(self): 33 | self.testfile = "test.ods" 34 | self.testfile2 = "test.csv" 35 | 36 | def tearDown(self): 37 | if os.path.exists(self.testfile): 38 | os.unlink(self.testfile) 39 | if os.path.exists(self.testfile2): 40 | os.unlink(self.testfile2) 41 | 42 | 43 | class TestodsHatWriter(PyexcelHatWriterBase): 44 | def setUp(self): 45 | self.testfile = "test.ods" 46 | 47 | def tearDown(self): 48 | if os.path.exists(self.testfile): 49 | os.unlink(self.testfile) 50 | 51 | 52 | def test_pr_28(): 53 | from datetime import datetime 54 | 55 | test_file = "pr28.ods" 56 | test = {"shee1": [[datetime(2022, 1, 30, 15, 45, 45)]]} 57 | writer = Writer(test_file, "ods") 58 | writer.write(test) 59 | writer.close() 60 | 61 | content = get_data(test_file) 62 | for key in content.keys(): 63 | content[key] = list(content[key]) 64 | eq_(content, {"shee1": [[datetime(2022, 1, 30, 15, 45, 45)]]}) 65 | 66 | os.unlink(test_file) 67 | --------------------------------------------------------------------------------