├── test ├── .gitignore └── test_plyfile.py ├── .gitignore ├── doc ├── api.md ├── install.md ├── rtd-bootstrap.sh ├── conf.py ├── index.md ├── faq.md ├── philosophy.md ├── developing.md ├── maintaining.md └── usage.md ├── .readthedocs.yaml ├── .github └── workflows │ ├── pull-request-doc-preview.yml │ └── python-package.yml ├── examples └── tet.ply ├── tox.ini ├── README.md ├── pyproject.toml ├── CHANGELOG.md ├── COPYING ├── plyfile.py └── pdm.lock /test/.gitignore: -------------------------------------------------------------------------------- 1 | test*.ply 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.swp 4 | *.egg-info 5 | plyfile-venv/ 6 | build/ 7 | dist/ 8 | .tox 9 | .cache 10 | .pytest_cache 11 | __pypackages__ 12 | .venv 13 | .pdm-python 14 | -------------------------------------------------------------------------------- /doc/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ```{eval-rst} 4 | .. automodule:: plyfile 5 | :members: 6 | :special-members: __init__, __getitem__, __setitem__, __len__, 7 | __contains__ 8 | ``` 9 | -------------------------------------------------------------------------------- /doc/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | pip3 install plyfile 4 | 5 | Or clone the [repository](https://github.com/dranjan/python-plyfile) and 6 | run from the project root: 7 | 8 | pip3 install . 9 | 10 | Or just copy `plyfile.py` into your GPL-compatible project. 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: doc/conf.py 5 | 6 | build: 7 | os: ubuntu-20.04 8 | tools: 9 | python: "3.12" 10 | jobs: 11 | post_install: 12 | # See https://github.com/pdm-project/pdm/discussions/1365 13 | - VIRTUAL_ENV=$(dirname $(dirname $(which python))) doc/rtd-bootstrap.sh 14 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-doc-preview.yml: -------------------------------------------------------------------------------- 1 | name: Documentation Preview 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | pull-request-links: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: readthedocs/actions/preview@v1 16 | with: 17 | project-slug: "python-plyfile" 18 | -------------------------------------------------------------------------------- /examples/tet.ply: -------------------------------------------------------------------------------- 1 | ply 2 | format ascii 1.0 3 | comment single tetrahedron with colored faces 4 | element vertex 4 5 | comment tetrahedron vertices 6 | property float x 7 | property float y 8 | property float z 9 | element face 4 10 | property list uchar int vertex_indices 11 | property uchar red 12 | property uchar green 13 | property uchar blue 14 | end_header 15 | 0 0 0 16 | 0 1 1 17 | 1 0 1 18 | 1 1 0 19 | 3 0 1 2 255 255 255 20 | 3 0 2 3 255 0 0 21 | 3 0 1 3 0 255 0 22 | 3 1 2 3 0 0 255 23 | -------------------------------------------------------------------------------- /doc/rtd-bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This script is used specifically in the Read the Docs build to 4 | # bootstrap the environment needed to generate the documentation (see 5 | # .readthedocs.yaml at the top level). A normal user should not need to 6 | # run this script except literally to test the logic below. 7 | 8 | set -euxo pipefail 9 | 10 | mkdir -p build/ 11 | curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/2.22.2/install-pdm.py > build/install-pdm.py 12 | mkdir -p build/root/ 13 | python build/install-pdm.py --path build/root -v 2.22.2 14 | build/root/bin/pdm install -dG doc 15 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | project = 'plyfile' 5 | copyright = '2014-2025, Darsh Ranjan and plyfile authors' 6 | author = 'Darsh Ranjan' 7 | extensions = [ 8 | 'sphinx.ext.autodoc', 9 | 'sphinx.ext.intersphinx', 10 | 'numpydoc', 11 | 'myst_parser', 12 | ] 13 | source_suffix = ['.rst', '.md'] 14 | templates_path = ['_templates'] 15 | html_theme = 'sphinx_rtd_theme' 16 | intersphinx_mapping = { 17 | "python": ("https://docs.python.org/3", None), 18 | 'numpy': ('http://docs.scipy.org/doc/numpy/', None), 19 | } 20 | autodoc_class_signature = "separated" 21 | autodoc_member_order = "bysource" 22 | numpydoc_class_members_toctree = False 23 | default_role = 'py:obj' 24 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | # plyfile 2 | 3 | Welcome to the `plyfile` Python module, which provides a simple facility 4 | for reading and writing ASCII and binary PLY files. 5 | 6 | The PLY format is documented 7 | [elsewhere][elsewhere]. 8 | 9 | [elsewhere]: https://web.archive.org/web/20161221115231/http://www.cs.virginia.edu/~gfx/Courses/2001/Advanced.spring.01/plylib/Ply.txt 10 | 11 | ## Table of Contents 12 | 13 | ```{toctree} 14 | 15 | install.md 16 | usage.md 17 | faq.md 18 | philosophy.md 19 | developing.md 20 | maintaining.md 21 | api.md 22 | 23 | ``` 24 | 25 | ## License 26 | 27 | Copyright 2014-2025 Darsh Ranjan and `plyfile` authors. 28 | 29 | This software is released under the terms of the GNU General Public 30 | License, version 3. See the file `COPYING` for details. 31 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = global-init,py39-numpy{2.0},py310-numpy{2.0,2.1,2.2},py311-numpy{2.0,2.1,2.2,2.3},py312-numpy{2.0,2.1,2.2,2.3},py313-numpy{2.1,2.2,2.3},global-finalize 3 | 4 | [gh-actions] 5 | python = 6 | 3.9: py39 7 | 3.10: py310 8 | 3.11: py311 9 | 3.12: py312 10 | 3.13: py313 11 | 12 | [testenv:global-init] 13 | skip_install = True 14 | usedevelop = False 15 | deps = 16 | coverage 17 | commands = coverage erase 18 | 19 | [testenv] 20 | usedevelop = True 21 | deps = 22 | pytest 23 | pytest-cov 24 | numpy2.0: numpy>=2.0,<2.1 25 | numpy2.1: numpy>=2.1,<2.2 26 | numpy2.2: numpy>=2.2,<2.3 27 | numpy2.3: numpy>=2.3,<2.4 28 | setenv = 29 | COVERAGE_FILE = {toxworkdir}/.coverage.{envname} 30 | commands = py.test test -v --cov=plyfile 31 | 32 | [testenv:global-finalize] 33 | skip_install = True 34 | usedevelop = False 35 | deps = 36 | coverage 37 | setenv = 38 | COVERAGE_FILE = {toxworkdir}/.coverage 39 | commands = 40 | coverage combine 41 | coverage html -d {toxworkdir}/htmlcov 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://github.com/dranjan/python-plyfile/actions/workflows/python-package.yml/badge.svg) 2 | 3 | Welcome to the `plyfile` Python module, which provides a simple facility 4 | for reading and writing ASCII and binary PLY files. 5 | 6 | # Quick start 7 | 8 | To install the latest official release: 9 | 10 | pip3 install plyfile 11 | 12 | To install from source: 13 | 14 | # From the project root 15 | pip3 install . 16 | 17 | # Quick links 18 | 19 | ## PLY format reference 20 | 21 | [Link](https://web.archive.org/web/20161221115231/http://www.cs.virginia.edu/~gfx/Courses/2001/Advanced.spring.01/plylib/Ply.txt) 22 | 23 | ## Project documentation 24 | 25 | [Link](https://python-plyfile.readthedocs.io) 26 | 27 | ## Getting help 28 | 29 | Have questions? Feel free to ask in 30 | [Discussions](https://github.com/dranjan/python-plyfile/discussions). 31 | 32 | ## Reporting bugs 33 | 34 | [Issues](https://github.com/dranjan/python-plyfile/issues) 35 | 36 | ## Contributing 37 | 38 | [Information for developers](https://python-plyfile.readthedocs.io/en/latest/developing.html) 39 | 40 | # Copyright and license 41 | 42 | Copyright Darsh Ranjan and `plyfile` authors. 43 | 44 | This software is released under the terms of the GNU General Public 45 | License, version 3. See the file `COPYING` for details. 46 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | pre_test: 11 | runs-on: ubuntu-latest 12 | outputs: 13 | should_skip: ${{ steps.skip_check.outputs.should_skip }} 14 | steps: 15 | - id: skip_check 16 | uses: fkirc/skip-duplicate-actions@v5 17 | with: 18 | do_not_skip: '["pull_request"]' 19 | test: 20 | needs: pre_test 21 | if: needs.pre_test.outputs.should_skip != 'true' 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install pipx 36 | python -m pipx install pdm 37 | pdm install -dG test --no-default 38 | - name: Test with tox 39 | # This runs just the selected interpreter, thanks to 40 | # the tox-gh-actions plugin we use. 41 | run: pdm run test-all 42 | -------------------------------------------------------------------------------- /doc/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | (faq-list-from-2d)= 4 | ## How do I initialize a list property from a two-dimensional array? 5 | 6 | ```Python Console 7 | >>> from plyfile import PlyElement 8 | >>> import numpy 9 | >>> 10 | >>> # Here's a two-dimensional array containing vertex indices. 11 | >>> face_data = numpy.array([[0, 1, 2], [3, 4, 5]], dtype='i4') 12 | >>> 13 | >>> # PlyElement.describe requires a one-dimensional structured array. 14 | >>> ply_faces = numpy.empty(len(face_data), 15 | ... dtype=[('vertex_indices', 'i4', (3,))]) 16 | >>> ply_faces['vertex_indices'] = face_data 17 | >>> face = PlyElement.describe(ply_faces, 'face') 18 | >>> 19 | ``` 20 | 21 | ## Can I save a PLY file directly to `sys.stdout`? 22 | 23 | Yes, for an ASCII-format PLY file. For binary-format files, it won't 24 | work directly, since `sys.stdout` is a text-mode stream and binary-format 25 | files can only be output to binary streams. (ASCII-format files can be 26 | output to text or binary streams.) 27 | 28 | There are a few ways around this. 29 | - Write to a named file instead. On Linux and some other Unix-likes, you 30 | can access `stdout` via the named file `/dev/stdout`: 31 | 32 | ```Python Console 33 | >>> plydata.write('/dev/stdout') # doctest: +SKIP 34 | ``` 35 | 36 | - Use `sys.stdout.buffer`: 37 | 38 | ```Python Console 39 | >>> plydata.write(sys.stdout.buffer) # doctest: +SKIP 40 | ``` 41 | 42 | ## Can I read a PLY file from `sys.stdin`? 43 | 44 | The answer is exactly analogous to the situation with writing to 45 | `sys.stdout`: it works for ASCII-format PLY files but not binary-format 46 | files. The two workarounds given above also apply: use a named file like 47 | `/dev/stdin`, or use `sys.stdin.buffer`. 48 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool] 2 | [tool.pdm] 3 | [tool.pdm.dev-dependencies] 4 | test = [ 5 | "tox>=4.4.8", 6 | "pytest>=7.2.2", 7 | "tox-gh-actions>=3.1.0", 8 | ] 9 | doc = [ 10 | "sphinx>=5.3.0", 11 | "numpydoc>=1.5.0", 12 | "myst-parser>=1.0.0", 13 | "sphinx-rtd-theme>=1.2.0", 14 | ] 15 | lint = [ 16 | "flake8>=3.9.2", 17 | ] 18 | 19 | [project] 20 | name = "plyfile" 21 | version = "1.1.3" 22 | description = "PLY file reader/writer" 23 | authors = [ 24 | {name = "Darsh Ranjan", email = "dranjan@berkeley.edu"}, 25 | ] 26 | dependencies = ["numpy>=1.21"] 27 | requires-python = ">=3.9" 28 | readme = "README.md" 29 | license = {file = "COPYING"} 30 | keywords = ["ply", "numpy"] 31 | classifiers = [ 32 | "Development Status :: 5 - Production/Stable", 33 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 34 | "Operating System :: OS Independent", 35 | "Programming Language :: Python :: 3", 36 | "Programming Language :: Python :: 3.9", 37 | "Programming Language :: Python :: 3.10", 38 | "Programming Language :: Python :: 3.11", 39 | "Programming Language :: Python :: 3.12", 40 | "Programming Language :: Python :: 3.13", 41 | "Topic :: Scientific/Engineering", 42 | ] 43 | 44 | [project.urls] 45 | Homepage = "https://github.com/dranjan/python-plyfile" 46 | Documentation = "https://python-plyfile.readthedocs.io" 47 | Repository = "https://github.com/dranjan/python-plyfile" 48 | 49 | [build-system] 50 | requires = ["pdm-backend"] 51 | build-backend = "pdm.backend" 52 | 53 | [tool.pdm.scripts] 54 | test-quick = "pytest test -v" 55 | test-matrix = "tox -v --skip-missing-interpreters=true" 56 | test-all = "tox -v --skip-missing-interpreters=false" 57 | doc = "sphinx-build doc doc/build -b html" 58 | lint = "flake8 plyfile.py test/test_plyfile.py" 59 | -------------------------------------------------------------------------------- /doc/philosophy.md: -------------------------------------------------------------------------------- 1 | # Design philosophy and rationale 2 | 3 | The design philosophy of `plyfile` can be summed up as follows. 4 | - Be familiar to users of `numpy` and reuse existing idioms and concepts 5 | when possible. 6 | - Favor simplicity over power or user-friendliness. 7 | - Support all valid PLY files. 8 | 9 | ## Familiarity 10 | 11 | For the most part, PLY concepts map nicely to Python and specifically to 12 | `numpy`, and leveraging that has strongly influenced the design of this 13 | package. The `elements` attribute of a `PlyData` instance is simply a 14 | `list` of `PlyElement` instances, and the `data` attribute of a 15 | `PlyElement` instance is a `numpy` array, and a list property field of a 16 | PLY element datum is referred to in the `data` attribute by a type of 17 | `object` with the value being another `numpy` array, etc. 18 | 19 | ## Simplicity 20 | 21 | When applicable, we favor simplicity over power or user-friendliness. 22 | Thus, list property types in `PlyElement.describe` always default to the 23 | same, rather than, say, being obtained by looking at an array element. 24 | (Which element? What if the array has length zero? Whatever default we 25 | could choose in that case could lead to subtle edge-case bugs if the 26 | user isn't vigilant.) Also, all input and output is done in "one shot": 27 | all the arrays must be created up front rather than being processed in a 28 | streaming fashion. 29 | 30 | ## Generality and interpretation issues 31 | 32 | We aim to support all valid PLY files. However, exactly what constitutes 33 | a "valid" file isn't obvious, since there doesn't seem to be a single 34 | complete and consistent description of the PLY format. Even the 35 | "authoritative" 36 | [Ply.txt](https://web.archive.org/web/20161221115231/http://www.cs.virginia.edu/~gfx/Courses/2001/Advanced.spring.01/plylib/Ply.txt) 37 | by Greg Turk has some issues. 38 | 39 | ### Comment placement 40 | 41 | Where can comments appear in the header? It appears that in all the 42 | "official" examples, all comments immediately follow the "format" line, 43 | but the language of the document neither places any such restrictions 44 | nor explicitly allows comments to be placed anywhere else. Thus, it 45 | isn't clear whether comments can appear anywhere in the header or must 46 | immediately follow the "format" line. At least one popular reader of 47 | PLY files chokes on comments before the "format" line. `plyfile` 48 | accepts comments anywhere in the header in input but only places them in 49 | a few limited places in output, namely immediately after "format" and 50 | "element" lines. 51 | 52 | ### Element and property names 53 | 54 | Another ambiguity is names: what strings are allowed as PLY element and 55 | property names? `plyfile` accepts as input any name that doesn't 56 | contain spaces, but this is surely too generous. This may not be such 57 | a big deal, though: although names are theoretically arbitrary, in 58 | practice, the majority of PLY element and property names probably come 59 | from a small finite set ("face", "x", "nx", "green", etc.). 60 | 61 | ### Property syntax 62 | 63 | A more serious problem is that the PLY format specification appears to 64 | be inconsistent regarding the syntax of property definitions. In 65 | some examples, it uses the syntax 66 | 67 | property {type} {name} 68 | 69 | and in others, 70 | 71 | property {name} {type} 72 | 73 | `plyfile` only supports the former, which appears to be standard _de 74 | facto_. 75 | 76 | ### Header line endings 77 | 78 | The specification explicitly states that lines in the header must 79 | end with carriage returns, but this rule doesn't seem to be followed by 80 | anyone, including the C-language PLY implementation by Greg Turk, the 81 | author of the format. Here, we stick to common practice and output 82 | Unix-style line endings (with no carriage returns) but accept any line 83 | ending style in input files. 84 | -------------------------------------------------------------------------------- /doc/developing.md: -------------------------------------------------------------------------------- 1 | # Information for Developers 2 | 3 | Thanks for your interest in contributing to the project! 4 | 5 | ## Source code repository 6 | 7 | [GitHub link](https://github.com/dranjan/python-plyfile) 8 | 9 | ## How to contribute code 10 | 11 | The easiest and recommended way to contribute code is to follow GitHub's 12 | usual pull request process. This will require a GitHub account. 13 | 14 | 1. Fork the repository into your own account. 15 | 2. Implement your changes on any branch. 16 | 3. Make a pull request on the main repository. The base branch should 17 | be `master`. 18 | 19 | A documentation preview link will automatically be edited into the pull 20 | request description. If you made any changes that could affect the 21 | documentation, you can follow the link to make sure things look as they 22 | should. You can also preview the documentation locally as described 23 | further down. 24 | 25 | ## General information 26 | 27 | - The implementation of the library is the single file `plyfile.py`. 28 | - The test suite is contained in `test/test_plyfile.py`. It uses the 29 | `pytest` framework. 30 | - The documentation text is contained in `doc/` and uses Sphinx to 31 | generate the HTML pages from MyST Markdown sources. 32 | The API reference is autogenerated directly from docstrings. 33 | - Project metadata is managed through [PDM](https://pdm.fming.dev) 34 | in `pyproject.toml`. 35 | Developer workflow tooling is also configured there (more information 36 | below). 37 | 38 | ## Contribution guidelines 39 | 40 | - Code style should follow [PEP-8](https://peps.python.org/pep-0008/). 41 | - Best practice is for new features and bug fixes to be accompanied by 42 | unit tests. 43 | - Bug fixes are welcome. 44 | - New features are generally accepted if they are consistent with the 45 | design and philosophy of the project. 46 | - However, extensions to the PLY format itself are generally not 47 | accepted, _unless_ they are already in widespread use in other PLY 48 | format libraries. 49 | 50 | ## Development environment setup 51 | 52 | The project implements a few workflow helper commands to simplify 53 | mundane activities like running tests, generating documentation, and 54 | linting. These are all implemented as PDM user scripts. You can check 55 | the `pyproject.toml` file to see exactly what they do. 56 | 57 | ### Installing PDM 58 | 59 | This project uses PDM to manage project metadata and development 60 | configurations. Thus, the first step is installing PDM using [any of the 61 | listed methods](https://pdm.fming.dev/latest/#installation). 62 | For example, 63 | 64 | ```none 65 | pip install pdm 66 | ``` 67 | 68 | or 69 | 70 | ```none 71 | pipx install pdm 72 | ``` 73 | 74 | ### Installing dependencies 75 | 76 | From the project root, 77 | 78 | ```none 79 | pdm install 80 | ``` 81 | 82 | ## Common actions 83 | 84 | The PDM user scripts below are defined in the `pyproject.toml` file, 85 | which should help you if want to know what each action exactly does. 86 | 87 | ### Running tests 88 | 89 | #### Quick 90 | 91 | To run the test suite on one Python interpreter and one version of 92 | NumPy: 93 | 94 | ```none 95 | pdm run test-quick 96 | ``` 97 | 98 | #### More comprehensive 99 | 100 | To run the full test matrix, skipping unavailable Python versions: 101 | 102 | ```none 103 | pdm run test-matrix 104 | ``` 105 | 106 | Test coverage will also be reported. 107 | 108 | #### Full 109 | 110 | To run the full test matrix: 111 | 112 | ```none 113 | pdm run test-all 114 | ``` 115 | 116 | You must have all currently supported Python versions to run this 117 | successfully. 118 | 119 | ### Generating documentation 120 | 121 | ```none 122 | pdm run doc 123 | ``` 124 | 125 | Open `doc/build/index.html` in any web browser to peruse the generated 126 | documentation. 127 | 128 | ### Running the linter 129 | 130 | ```none 131 | pdm run lint 132 | ``` 133 | 134 | Generally speaking, there should be no violations reported for the code 135 | to be considered mergeable. 136 | 137 | ## NumPy and Python version support 138 | 139 | Our rule of thumb is that we support and test against all currently supported 140 | NumPy versions and all Python versions officially supported by them, and 141 | the test runner setup (`tox.ini` and 142 | `.github/workflows/python-package.yml`) should be kept updated to 143 | reflect this. 144 | 145 | The currently supported NumPy versions are stated in 146 | [NEP 29](https://numpy.org/neps/nep-0029-deprecation_policy.html). 147 | We look at the package metadata to determine which Python versions they 148 | officially support, which can be easily checked on PyPI. (Example: 149 | [Numpy 2.0](https://pypi.org/project/numpy/2.0.0/).) 150 | 151 | All that being said, we consider missing a NumPy or Python release to 152 | be pretty low-risk, so at any given moment, our official test matrix 153 | may be lagging the latest releases by a little bit. 154 | -------------------------------------------------------------------------------- /doc/maintaining.md: -------------------------------------------------------------------------------- 1 | # Information for Project Maintainers 2 | 3 | This information is for **project maintainers**. If you are simply 4 | contributing to the project, then you don't need to know this, and you 5 | don't need to perform any of these steps. 6 | 7 | ## Merging into `master` 8 | 9 | Merging into `master` uses Git's default merging mechanism. We don't use 10 | rebase-merges or squash-merges. Fast-forward merges are allowed, however, 11 | and encouraged when applicable. 12 | 13 | Merging should always be performed locally on the maintainer's system, 14 | and then pushed directly to the `master` branch on GitHub. If the merge 15 | was sufficiently complicated that it would be beneficial to run the 16 | GitHub actions again on the merged result, then merge first into a 17 | temporary test branch, make a new pull request from that, and finally 18 | fast-forward `master` when the result is acceptable. 19 | 20 | Note that updating `master` on GitHub _does not_ automatically imply a 21 | release must be made and published to PyPI, but the converse does hold: 22 | releases are only made from the tip of `master`. 23 | 24 | ## Making a new release 25 | 26 | Making a release is a fairly manual process, but it's not too onerous and 27 | doesn't happen very often, so there isn't much impetus to automate it. 28 | 29 | Releases are always made from the `master` branch. Thus, begin by checking 30 | out the tip of `master` from GitHub. 31 | 32 | ### Pre-release checks 33 | 34 | The following conditions must be met before the release can be made. 35 | 36 | - The code is in a releasable state: 37 | - unit tests pass, 38 | - linting reports no violations, and 39 | - the documentation is up to date. 40 | - The change log (`CHANGELOG.md`) is up to date. 41 | 42 | If necessary, make additional commits on `master` until the conditions 43 | are met. The steps that follow assume that the pre-release conditions 44 | are satisfied on the tip of `master`. 45 | 46 | ### Increment the version 47 | 48 | Incrementing the version is done via a small tagged commit. The 49 | information in this section can be summarized succinctly by looking at 50 | one of these commits: 51 | 52 | ```bash 53 | git show v0.8.1 54 | ``` 55 | 56 | We'll also briefly describe all the steps here. 57 | 58 | #### Select a new version 59 | 60 | Make sure to follow [PEP-440](https://peps.python.org/pep-0440/). 61 | In this example, `v0.8` is the old version, and `v0.8.1` is the new 62 | version. 63 | 64 | Currently, we don't publish alpha or beta releases to PyPI, with the 65 | understanding that prereleases can be checked out directly from GitHub. 66 | 67 | #### Update the version in `pyproject.toml` 68 | 69 | One line is changed: 70 | 71 | ```diff 72 | diff --git a/pyproject.toml b/pyproject.toml 73 | index d079b17..5c42074 100644 74 | --- a/pyproject.toml 75 | +++ b/pyproject.toml 76 | @@ -3,7 +3,7 @@ 77 | 78 | [project] 79 | name = "plyfile" 80 | -version = "0.8" 81 | +version = "0.8.1" 82 | description = "PLY file reader/writer" 83 | authors = [ 84 | ``` 85 | 86 | #### Update `CHANGELOG.md` 87 | 88 | This should be pretty self-explanatory: 89 | 90 | ```diff 91 | diff --git a/CHANGELOG.md b/CHANGELOG.md 92 | index 03dba5f..22ee9a5 100644 93 | --- a/CHANGELOG.md 94 | +++ b/CHANGELOG.md 95 | @@ -3,6 +3,8 @@ 96 | All notable changes to this project will be documented here. 97 | 98 | ## [Unreleased] 99 | + 100 | +## [0.8.1] - 2023-03-18 101 | ### Changed 102 | - Package metadata management via PDM, rather than `setuptools`. 103 | 104 | @@ -117,7 +119,8 @@ All notable changes to this project will be documented here. 105 | - Rudimentary test setup. 106 | - Basic installation script. 107 | 108 | -[Unreleased]: https://github.com/dranjan/python-plyfile/compare/v0.8...HEAD 109 | +[Unreleased]: https://github.com/dranjan/python-plyfile/compare/v0.8.1...HEAD 110 | +[0.8.1]: https://github.com/dranjan/python-plyfile/compare/v0.8...v0.8.1 111 | [0.8]: https://github.com/dranjan/python-plyfile/compare/v0.7.4...v0.8 112 | [0.7.4]: https://github.com/dranjan/python-plyfile/compare/v0.7.3...v0.7.4 113 | [0.7.3]: https://github.com/dranjan/python-plyfile/compare/v0.7.2...v0.7.3 114 | ``` 115 | 116 | A new heading containing a link is added. No content is added. 117 | 118 | #### Make the commit 119 | 120 | ```bash 121 | git add pyproject.toml 122 | git add CHANGELOG.md 123 | git commit -m "Bump version" 124 | ``` 125 | 126 | The commit message is exactly as above. 127 | 128 | #### Tag the commit 129 | 130 | ```bash 131 | git tag -a v0.8.1 132 | ``` 133 | 134 | The tag annotation should look like this, with only the `0.8.1` string 135 | changing from version to version: 136 | 137 | ```none 138 | Version 0.8.1 139 | 140 | See CHANGELOG.md for details. 141 | ``` 142 | 143 | ### Publish the new version 144 | 145 | Prerequisite: generate a API token on PyPI and save it somewhere. 146 | This example will assume the file is saved as `token.txt`. 147 | (PyPI no longer supports normal username/password authentication, 148 | so this step must be performed.) 149 | 150 | ```bash 151 | pdm publish -u __token__ -P $(< token.txt) 152 | ``` 153 | 154 | To use the test server, add the arguments `-r testpypi`. Note that 155 | a separate API token will be required, generated from your account 156 | on the test server. 157 | 158 | ### Publish the code to GitHub 159 | 160 | ```bash 161 | git push origin master v0.8.1 162 | ``` 163 | 164 | (Don't forget to push both the branch and the tag, as shown above!) 165 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented here. 4 | 5 | ## [1.1.3] - 2025-10-21 6 | ### Changed 7 | - Skip empty header lines to improve interoperability. Thanks to @JTvD 8 | for the change. 9 | 10 | ## [1.1.2] - 2025-06-01 11 | ### Changed 12 | - Minor metadata updates. 13 | 14 | ## [1.1.1] - 2025-05-31 15 | ### Added 16 | - Official support for Python 3.13. 17 | - Official support for NumPy 2.1 and NumPy 2.2. 18 | 19 | ### Changed 20 | - Build backend from `pdm-pep517` to `pdm-backend`. Thanks to @bcbnz for 21 | the fix. 22 | 23 | ### Fixed 24 | - Integer overflow when reading large files. Thanks to @cdcseacave for 25 | the fix. 26 | 27 | ### Removed 28 | - Official support for NumPy < 1.25. 29 | 30 | ## [1.1] - 2024-08-04 31 | ### Added 32 | - Official support for NumPy 2.0. 33 | - Support for write-through memory-mapping. Thanks to @nh2 and 34 | @chpatrick for the original implementation. 35 | 36 | ### Fixed 37 | - A small unit test bug. 38 | 39 | ### Removed 40 | - Official support for Python 3.8. 41 | - Official support for NumPy < 1.21. 42 | 43 | ## [1.0.3] - 2024-01-06 44 | ### Fixed 45 | - Maintainers' documentation for publishing. 46 | 47 | ## [1.0.2] - 2023-11-13 48 | ### Added 49 | - Official support for Python 3.12 and `numpy` 1.26. 50 | 51 | ## [1.0.1] - 2023-07-23 52 | ### Changed 53 | - Minor change to project metadata. 54 | 55 | ## [1.0] - 2023-07-09 56 | ### Added 57 | - PDM user scripts for common development workflow tasks. 58 | - New guides for contributing and maintaining. 59 | - `.readthedocs.yaml` configuration for documentation hosting on 60 | readthedocs.io. 61 | - Official support for Python 3.11 and `numpy` 1.25. 62 | 63 | ### Changed 64 | - Major documentation overhaul: 65 | - documentation moved from `README.md` to `doc/`; 66 | - Sphinx configuration to render documentation nicely as HTML, 67 | including API reference via `autodoc`. 68 | - Reordering of code in `plyfile.py`. 69 | 70 | ### Removed 71 | - `examples/plot.py`, which was a bit out of place. 72 | - Official support for Python 3.7. 73 | 74 | ## [0.9] - 2023-04-12 75 | ### Added 76 | - Support for reading ASCII-format PLY files from text streams. 77 | - Doctest runner in unit test suite. 78 | 79 | ### Changed 80 | - Docstring formatting style: 81 | - better PEP-257 compliance; 82 | - adoption of NumPy docstring style. 83 | 84 | ### Fixed 85 | - Support for reading Mac-style line endings. 86 | 87 | ### Removed 88 | - Python2-specific code paths. 89 | - `make2d` function (redundant with `numpy.vstack`). 90 | 91 | ## [0.8.1] - 2023-03-18 92 | ### Changed 93 | - Package metadata management via PDM, rather than `setuptools`. 94 | 95 | ### Fixed 96 | - Project classifiers array. 97 | 98 | ## [0.8] - 2023-03-17 99 | ### Added 100 | - `known_list_len` optional argument. Thanks to @markbandstra. 101 | 102 | ### Removed 103 | - Official support for Python<3.7 and `numpy`<1.17. 104 | 105 | ## [0.7.4] - 2021-05-02 106 | ### Fixed 107 | - `DeprecationWarning` fix on `numpy`>=1.19. Thanks to @markbandstra. 108 | 109 | ## [0.7.3] - 2021-02-06 110 | ### Added 111 | - Memory-mapping made optional. 112 | - FAQ section in `README.md`. 113 | - `PlyElement.__len__` and `PlyElement.__contains__`. Thanks to 114 | @athompson673. 115 | 116 | ### Changed 117 | - Syntax highlighting in `README.md` improved. 118 | 119 | ## [0.7.2] - 2020-03-21 120 | ### Added 121 | - Long description added to package distribution. 122 | 123 | ## [0.7.1] - 2019-10-08 124 | ### Fixed 125 | - License file included in distribution. 126 | 127 | ## [0.7] - 2018-12-25 128 | ### Added 129 | - Read & write support for file-like objects. 130 | 131 | ### Fixed 132 | - Documentation improved. 133 | 134 | ## [0.6] - 2018-07-28 135 | ### Changed 136 | - Changed line endings back to Unix-style. 137 | 138 | ### Fixed 139 | - `make2d` on `numpy`>=1.14. 140 | 141 | ### Deprecated 142 | - `make2d` function. Please use `numpy.vstack`. 143 | 144 | ## [0.5] - 2017-02-27 145 | ### Added 146 | - Project metadata suitable for PyPI. 147 | - More unit tests. 148 | - Official support for NumPy 1.10 and 1.11. 149 | - Automatic unit test coverage reporting through test runner. 150 | - CHANGELOG.md. 151 | - Comment validation and preservation of leading spaces. Thanks to 152 | @Zac-HD for the bug report. 153 | - Better validation of element and property names. 154 | 155 | ### Changed 156 | - Made "private" variables explicitly so. 157 | - Used memory mapping for "simple" PLY elements. Thanks to @Zac-HD for 158 | the original pull request. 159 | - (Under the hood) Rewrote header parser. 160 | 161 | ### Removed 162 | - Official support for Python 2.6. 163 | 164 | ### Fixed 165 | - Fixed reading and writing through unicode filenames. 166 | - Fixed documentation bugs. Thanks to @jeremyherbert. 167 | 168 | ## [0.4] - 2015-04-05 169 | ### Added 170 | - `PlyParseError` for parsing errors. 171 | - Explicit (limited) mutability of PLY metadata. 172 | - Better validation when modifying PLY metadata. 173 | - More unit tests. 174 | 175 | ### Removed 176 | - Ability to change element and property names by mutation, which was 177 | never handled correctly anyway. 178 | 179 | ## [0.3] - 2015-03-28 180 | ### Added 181 | - `__getitem__` and `__setitem__` for `PlyElement`. 182 | - Support for `obj_info` comments. 183 | - `make2d` utility function. 184 | 185 | ### Changed 186 | - Ported test setup to `py.test` and `tox`. 187 | - Changed output property names to those in original specification. 188 | 189 | ## [0.2] - 2014-11-09 190 | ### Added 191 | - GPLv3 license. 192 | - Documentation. 193 | - Example plotting script. 194 | - Python 3 compatibility. Thanks to @svenpilz for most of the bug fixes. 195 | 196 | ### Fixed 197 | - Changed line endings to be compliant with specification. 198 | - Improved validation of property and element names. 199 | 200 | ## 0.1 - 2014-05-17 201 | ### Added 202 | - plyfile.py: PLY format I/O. 203 | - Rudimentary test setup. 204 | - Basic installation script. 205 | 206 | [Unreleased]: https://github.com/dranjan/python-plyfile/compare/v1.1.3...HEAD 207 | [1.1.3]: https://github.com/dranjan/python-plyfile/compare/v1.1.2...v1.1.3 208 | [1.1.2]: https://github.com/dranjan/python-plyfile/compare/v1.1.1...v1.1.2 209 | [1.1.1]: https://github.com/dranjan/python-plyfile/compare/v1.1...v1.1.1 210 | [1.1]: https://github.com/dranjan/python-plyfile/compare/v1.0.3...v1.1 211 | [1.0.3]: https://github.com/dranjan/python-plyfile/compare/v1.0.2...v1.0.3 212 | [1.0.2]: https://github.com/dranjan/python-plyfile/compare/v1.0.1...v1.0.2 213 | [1.0.1]: https://github.com/dranjan/python-plyfile/compare/v1.0...v1.0.1 214 | [1.0]: https://github.com/dranjan/python-plyfile/compare/v0.9...v1.0 215 | [0.9]: https://github.com/dranjan/python-plyfile/compare/v0.8.1...v0.9 216 | [0.8.1]: https://github.com/dranjan/python-plyfile/compare/v0.8...v0.8.1 217 | [0.8]: https://github.com/dranjan/python-plyfile/compare/v0.7.4...v0.8 218 | [0.7.4]: https://github.com/dranjan/python-plyfile/compare/v0.7.3...v0.7.4 219 | [0.7.3]: https://github.com/dranjan/python-plyfile/compare/v0.7.2...v0.7.3 220 | [0.7.2]: https://github.com/dranjan/python-plyfile/compare/v0.7.1...v0.7.2 221 | [0.7.1]: https://github.com/dranjan/python-plyfile/compare/v0.7...v0.7.1 222 | [0.7]: https://github.com/dranjan/python-plyfile/compare/v0.6...v0.7 223 | [0.6]: https://github.com/dranjan/python-plyfile/compare/v0.5...v0.6 224 | [0.5]: https://github.com/dranjan/python-plyfile/compare/v0.4...v0.5 225 | [0.4]: https://github.com/dranjan/python-plyfile/compare/v0.3...v0.4 226 | [0.3]: https://github.com/dranjan/python-plyfile/compare/v0.2...v0.3 227 | [0.2]: https://github.com/dranjan/python-plyfile/compare/v0.1...v0.2 228 | -------------------------------------------------------------------------------- /doc/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Both deserialization and serialization of PLY file data is done through 4 | `PlyData` and `PlyElement` instances. 5 | 6 | ```Python Console 7 | >>> import numpy 8 | >>> from plyfile import PlyData, PlyElement 9 | >>> 10 | ``` 11 | 12 | For the code examples that follow, assume the file `tet.ply` contains 13 | the following text: 14 | 15 | ply 16 | format ascii 1.0 17 | comment single tetrahedron with colored faces 18 | element vertex 4 19 | comment tetrahedron vertices 20 | property float x 21 | property float y 22 | property float z 23 | element face 4 24 | property list uchar int vertex_indices 25 | property uchar red 26 | property uchar green 27 | property uchar blue 28 | end_header 29 | 0 0 0 30 | 0 1 1 31 | 1 0 1 32 | 1 1 0 33 | 3 0 1 2 255 255 255 34 | 3 0 2 3 255 0 0 35 | 3 0 1 3 0 255 0 36 | 3 1 2 3 0 0 255 37 | 38 | (This file is available under the `examples` directory.) 39 | 40 | ## Reading a PLY file 41 | 42 | ```Python Console 43 | >>> plydata = PlyData.read('tet.ply') 44 | >>> 45 | ``` 46 | 47 | or 48 | 49 | ```Python Console 50 | >>> with open('tet.ply', 'rb') as f: 51 | ... plydata = PlyData.read(f) 52 | >>> 53 | ``` 54 | 55 | The static method `PlyData.read` returns a `PlyData` instance, which is 56 | `plyfile`'s representation of the data in a PLY file. A `PlyData` 57 | instance has an attribute `elements`, which is a list of `PlyElement` 58 | instances, each of which has a `data` attribute which is a `numpy` 59 | structured array containing the numerical data. PLY file elements map 60 | onto `numpy` structured arrays in a pretty obvious way. For a list 61 | property in an element, by default, the corresponding `numpy` field type 62 | is `object`, with the members being `numpy` arrays (see the 63 | `vertex_indices` example below).[^list_property_note] 64 | 65 | [^list_property_note]: Also see the 66 | [section on `known_list_len`](#known_list_len). 67 | 68 | Concretely: 69 | 70 | ```Python Console 71 | >>> plydata.elements[0].name 72 | 'vertex' 73 | >>> plydata.elements[0].data[0].tolist() 74 | (0.0, 0.0, 0.0) 75 | >>> plydata.elements[0].data['x'] 76 | array([0., 0., 1., 1.], dtype=float32) 77 | >>> plydata['face'].data['vertex_indices'][0] 78 | array([0, 1, 2], dtype=int32) 79 | >>> 80 | ``` 81 | 82 | For convenience, elements and properties can be looked up by name: 83 | 84 | ```Python Console 85 | >>> plydata['vertex']['x'] 86 | array([0., 0., 1., 1.], dtype=float32) 87 | >>> 88 | ``` 89 | 90 | and elements can be indexed directly without explicitly going through 91 | the `data` attribute: 92 | 93 | ```Python Console 94 | >>> plydata['vertex'][0].tolist() 95 | (0.0, 0.0, 0.0) 96 | >>> 97 | ``` 98 | 99 | The above expression is equivalent to `plydata['vertex'].data[0]`. 100 | 101 | `PlyElement` instances also contain metadata: 102 | 103 | ```Python Console 104 | >>> plydata.elements[0].properties 105 | (PlyProperty('x', 'float'), PlyProperty('y', 'float'), PlyProperty('z', 'float')) 106 | >>> plydata.elements[0].count 107 | 4 108 | >>> 109 | ``` 110 | 111 | `PlyProperty` and `PlyListProperty` instances are used internally as a 112 | convenient intermediate representation of PLY element properties that 113 | can easily be serialized to a PLY header (using `str`) or converted to 114 | `numpy`-compatible type descriptions (via the `dtype` method). It's not 115 | extremely common to manipulate them directly, but if needed, the 116 | property metadata of an element can be accessed as a tuple via the 117 | `properties` attribute (as illustrated above) or looked up by name: 118 | 119 | ```Python Console 120 | >>> plydata.elements[0].ply_property('x') 121 | PlyProperty('x', 'float') 122 | >>> 123 | ``` 124 | 125 | Many (but not necessarily all) types of malformed input files will raise 126 | `PlyParseError` when `PlyData.read` is called. The string value of the 127 | `PlyParseError` instance (as well as attributes `element`, `row`, and 128 | `prop`) provides additional context for the error if applicable. 129 | 130 | ### Faster reading via memory mapping 131 | 132 | To accelerate parsing of binary data, `plyfile` can make use of 133 | `numpy`'s memory mapping facilities. The decision to memory map or not 134 | is made on a per-element basis. To make this determination, there are 135 | two cases to consider. 136 | 137 | #### Case 1: elements with no list properties 138 | 139 | If an element in a binary PLY file has no list properties, then it will 140 | be memory-mapped by default, subject to the capabilities of the 141 | underlying file object. 142 | - Memory mapping can be disabled or fine-tuned using the `mmap` argument 143 | of `PlyData.read`. 144 | - To confirm whether a given element has been memory-mapped or not, 145 | check the type of `element.data`. 146 | 147 | This is all illustrated below: 148 | 149 | ```Python Console 150 | >>> plydata.text = False 151 | >>> plydata.byte_order = '<' 152 | >>> plydata.write('tet_binary.ply') 153 | >>> 154 | >>> # Memory-mapping is enabled by default. 155 | >>> plydata = PlyData.read('tet_binary.ply') 156 | >>> isinstance(plydata['vertex'].data, numpy.memmap) 157 | True 158 | >>> # Any falsy value disables memory-mapping here. 159 | >>> plydata = PlyData.read('tet_binary.ply', mmap=False) 160 | >>> isinstance(plydata['vertex'].data, numpy.memmap) 161 | False 162 | >>> # Strings can also be given to fine-tune memory-mapping. 163 | >>> # For example, with 'r+', changes can be written back to the file. 164 | >>> # In this case, the file must be explicitly opened with read-write 165 | >>> # access. 166 | >>> with open('tet_binary.ply', 'r+b') as f: 167 | ... plydata = PlyData.read(f, mmap='r+') 168 | >>> isinstance(plydata['vertex'].data, numpy.memmap) 169 | True 170 | >>> plydata['vertex']['x'] = 100 171 | >>> plydata['vertex'].data.flush() 172 | >>> plydata = PlyData.read('tet_binary.ply') 173 | >>> all(plydata['vertex']['x'] == 100) 174 | True 175 | >>> 176 | ``` 177 | 178 | (known_list_len)= 179 | #### Case 2: elements with list properties 180 | 181 | In the general case, elements with list properties cannot be 182 | memory-mapped as `numpy` arrays, except in one important case: when 183 | all list properties have fixed and known lengths. In that case, the 184 | `known_list_len` argument can be given to `PlyData.read`: 185 | 186 | ```Python Console 187 | >>> plydata = PlyData.read('tet_binary.ply', 188 | ... known_list_len={'face': {'vertex_indices': 3}}) 189 | >>> isinstance(plydata['face'].data, numpy.memmap) 190 | True 191 | >>> 192 | ``` 193 | 194 | The implementation will validate the data: if any instance of the list 195 | property has a length other than the value specified, then 196 | `PlyParseError` will be raised. 197 | 198 | Note that in order to enable memory mapping for a given element, 199 | *all* list properties in the element must have their lengths in the 200 | `known_list_len` dictionary. If any list property does not have its 201 | length given in `known_list_len`, then memory mapping will not be 202 | attempted, and no error will be raised. 203 | 204 | ## Creating a PLY file 205 | 206 | The first step is to get your data into `numpy` structured arrays. Note 207 | that there are some restrictions: generally speaking, if you know the 208 | types of properties a PLY file element can contain, you can easily 209 | deduce the restrictions. For example, PLY files don't contain 64-bit 210 | integer or complex data, so these aren't allowed. 211 | 212 | For convenience, non-scalar fields **are** allowed, and they will be 213 | serialized as list properties. For example, when constructing a "face" 214 | element, if all the faces are triangles (a common occurrence), it's okay 215 | to have a "vertex_indices" field of type `'i4'` and shape `(3,)` 216 | instead of type `object` and shape `()`. However, if the serialized PLY 217 | file is read back in using `plyfile`, the "vertex_indices" property will 218 | be represented as an `object`-typed field, each of whose values is an 219 | array of type `'i4'` and length 3. The reason is simply that the PLY 220 | format provides no way to find out that each "vertex_indices" field has 221 | length 3 without actually reading all the data, so `plyfile` has to 222 | assume that this is a variable-length property. However, see the 223 | [FAQ](#faq-list-from-2d) for an easy way to recover a two-dimensional 224 | array from a list property, and also see the [notes above](#known_list_len) 225 | about the `known_list_len` parameter to speed up the reading of files with 226 | lists of fixed, known length. 227 | 228 | For example, if we wanted to create the "vertex" and "face" PLY elements 229 | of the `tet.ply` data directly as `numpy` arrays for the purpose of 230 | serialization, we could do this: 231 | 232 | ```Python Console 233 | >>> vertex = numpy.array([(0, 0, 0), 234 | ... (0, 1, 1), 235 | ... (1, 0, 1), 236 | ... (1, 1, 0)], 237 | ... dtype=[('x', 'f4'), ('y', 'f4'), 238 | ... ('z', 'f4')]) 239 | >>> face = numpy.array([([0, 1, 2], 255, 255, 255), 240 | ... ([0, 2, 3], 255, 0, 0), 241 | ... ([0, 1, 3], 0, 255, 0), 242 | ... ([1, 2, 3], 0, 0, 255)], 243 | ... dtype=[('vertex_indices', 'i4', (3,)), 244 | ... ('red', 'u1'), ('green', 'u1'), 245 | ... ('blue', 'u1')]) 246 | >>> 247 | ``` 248 | 249 | Once you have suitably structured array, the static method 250 | `PlyElement.describe` can then be used to create the necessary 251 | `PlyElement` instances: 252 | 253 | ```Python Console 254 | >>> el = PlyElement.describe(vertex, 'vertex') 255 | >>> 256 | ``` 257 | 258 | or 259 | 260 | ```Python Console 261 | >>> el = PlyElement.describe(vertex, 'vertex', 262 | ... comments=['comment1', 263 | ... 'comment2']) 264 | >>> 265 | ``` 266 | 267 | Note that there's no need to create `PlyProperty` instances explicitly. 268 | This is all done behind the scenes by examining `some_array.dtype.descr`. 269 | One slight hiccup here is that variable-length fields in a `numpy` array 270 | (i.e., our representation of PLY list properties) 271 | must have a type of `object`, so the types of the list length and values 272 | in the serialized PLY file can't be obtained from the array's `dtype` 273 | attribute alone. For simplicity and predictability, the length 274 | defaults to 8-bit unsigned integer, and the value defaults to 32-bit 275 | signed integer, which covers the majority of use cases. Exceptions must 276 | be stated explicitly: 277 | 278 | ```Python Console 279 | >>> el = PlyElement.describe(face, 'face', 280 | ... val_types={'vertex_indices': 'u2'}, 281 | ... len_types={'vertex_indices': 'u4'}) 282 | >>> 283 | ``` 284 | 285 | Now you can instantiate `PlyData` and serialize: 286 | 287 | ```Python Console 288 | >>> PlyData([el]).write('some_binary.ply') 289 | >>> PlyData([el], text=True).write('some_ascii.ply') 290 | >>> 291 | >>> # Force the byte order of the output to big-endian, independently of 292 | >>> # the machine's native byte order 293 | >>> PlyData([el], 294 | ... byte_order='>').write('some_big_endian_binary.ply') 295 | >>> 296 | >>> # Use a file object. Binary mode is used here, which will cause 297 | >>> # Unix-style line endings to be written on all systems. 298 | >>> with open('some_ascii.ply', mode='wb') as f: 299 | ... PlyData([el], text=True).write(f) 300 | >>> 301 | ``` 302 | 303 | ## Miscellaneous 304 | 305 | ### Comments 306 | 307 | Header comments are supported: 308 | 309 | ```Python Console 310 | >>> ply = PlyData([el], comments=['header comment']) 311 | >>> ply.comments 312 | ['header comment'] 313 | >>> 314 | ``` 315 | 316 | `obj_info` comments are supported as well: 317 | 318 | ```Python Console 319 | >>> ply = PlyData([el], obj_info=['obj_info1', 'obj_info2']) 320 | >>> ply.obj_info 321 | ['obj_info1', 'obj_info2'] 322 | >>> 323 | ``` 324 | 325 | When written, they will be placed after regular comments after the 326 | "format" line. 327 | 328 | Comments can have leading whitespace, but trailing whitespace may be 329 | stripped and should not be relied upon. Comments may not contain 330 | embedded newlines. 331 | 332 | ### Getting a two-dimensional array from a list property 333 | 334 | The PLY format provides no way to assert that all the data for a given 335 | list property is of the same length, yet this is a relatively common 336 | occurrence. For example, all the "vertex_indices" data on a "face" 337 | element will have length three for a triangular mesh. In such cases, 338 | it's usually much more convenient to have the data in a two-dimensional 339 | array, as opposed to a one-dimensional array of type `object`. Here's a 340 | pretty easy way to obtain a two dimensional array: 341 | 342 | ```Python Console 343 | >>> plydata = PlyData.read('tet.ply') 344 | >>> tri_data = plydata['face'].data['vertex_indices'] 345 | >>> triangles = numpy.vstack(tri_data) 346 | >>> 347 | ``` 348 | 349 | (If the row lengths of all list properties are known in advance, the 350 | [`known_list_len` parameter](#known_list_len) can also be used.) 351 | 352 | ### Instance mutability 353 | 354 | A plausible code pattern is to read a PLY file into a `PlyData` 355 | instance, perform some operations on it, possibly modifying data and 356 | metadata in place, and write the result to a new file. This pattern is 357 | partially supported. The following in-place mutations are possible: 358 | 359 | - Modifying numerical array data only. 360 | - Assigning directly to a `PlyData` instance's `elements`. 361 | - Switching format by changing the `text` and `byte_order` attributes of 362 | a `PlyData` instance. This will switch between `ascii`, 363 | `binary_little_endian`, and `binary_big_endian` PLY formats. 364 | - Modifying a `PlyData` instance's `comments` and `obj_info`, and 365 | modifying a `PlyElement` instance's `comments`. 366 | - Assigning to an element's `data`. Note that the property metadata in 367 | `properties` is not touched by this, so for every property in the 368 | `properties` list of the `PlyElement` instance, the `data` array must 369 | have a field with the same name (but possibly different type, and 370 | possibly in different order). The array can have additional fields as 371 | well, but they won't be output when writing the element to a PLY file. 372 | The properties in the output file will appear as they are in the 373 | `properties` list. If an array field has a different type than the 374 | corresponding `PlyProperty` instance, then it will be cast when 375 | writing. 376 | - Assigning directly to an element's `properties`. Note that the 377 | `data` array is not touched, and the previous note regarding the 378 | relationship between `properties` and `data` still applies: the field 379 | names of `data` must be a superset of the property names in 380 | `properties`, but they can be in a different order and specify 381 | different types. 382 | - Changing a `PlyProperty` or `PlyListProperty` instance's `val_dtype` 383 | or a `PlyListProperty` instance's `len_dtype`, which will perform 384 | casting when writing. 385 | 386 | Modifying the `name` of a `PlyElement`, `PlyProperty`, or 387 | `PlyListProperty` instance is not supported and will raise an error. To 388 | rename a property of a `PlyElement` instance, you can remove the 389 | property from `properties`, rename the field in `data`, and re-add the 390 | property to `properties` with the new name by creating a new 391 | `PlyProperty` or `PlyListProperty` instance: 392 | 393 | ```Python Console 394 | >>> from plyfile import PlyProperty, PlyListProperty 395 | >>> face = plydata['face'] 396 | >>> face.properties = () 397 | >>> face.data.dtype.names = ['idx', 'r', 'g', 'b'] 398 | >>> face.properties = (PlyListProperty('idx', 'uchar', 'int'), 399 | ... PlyProperty('r', 'uchar'), 400 | ... PlyProperty('g', 'uchar'), 401 | ... PlyProperty('b', 'uchar')) 402 | >>> 403 | ``` 404 | 405 | Note that it is always safe to create a new `PlyElement` or `PlyData` 406 | instance instead of modifying one in place, and this is the recommended 407 | style: 408 | 409 | ```Python Console 410 | >>> # Recommended: 411 | >>> plydata = PlyData([plydata['face'], plydata['vertex']], 412 | ... text=False, byte_order='<') 413 | >>> 414 | >>> # Also supported: 415 | >>> plydata.elements = [plydata['face'], plydata['vertex']] 416 | >>> plydata.text = False 417 | >>> plydata.byte_order = '<' 418 | >>> plydata.comments = [] 419 | >>> plydata.obj_info = [] 420 | >>> 421 | ``` 422 | 423 | Objects created by this library don't claim ownership of the other 424 | objects they refer to, which has implications for both styles (creating 425 | new instances and modifying in place). For example, a single 426 | `PlyElement` instance can be contained by multiple `PlyData` instances, 427 | but modifying that instance will then affect all of those containing 428 | `PlyData` instances. 429 | 430 | ### Text-mode streams 431 | 432 | Input and output on text-mode streams is supported for ASCII-format 433 | PLY files, but not binary-format PLY files. Input and output on 434 | binary streams is supported for all valid PLY files. Note that 435 | `sys.stdout` and `sys.stdin` are text streams, so they can only be 436 | used directly for ASCII-format PLY files. 437 | -------------------------------------------------------------------------------- /test/test_plyfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import doctest 4 | from io import (BytesIO, StringIO, TextIOWrapper) 5 | import gzip 6 | 7 | import pytest 8 | 9 | import numpy 10 | 11 | import plyfile 12 | from plyfile import ( 13 | PlyData, PlyElement, PlyProperty, 14 | PlyHeaderParseError, PlyElementParseError, 15 | ) 16 | 17 | 18 | class Raises(object): 19 | """ 20 | Context manager for code excpected to raise an exception. 21 | 22 | Exception information is only available after the context has been 23 | exited due to a raised exception. 24 | 25 | Attributes 26 | ---------- 27 | exc_type : type 28 | The exception type that was raised. 29 | exc_val : Exception 30 | The raised exception. 31 | traceback : traceback 32 | """ 33 | 34 | def __init__(self, *exc_types): 35 | """ 36 | Parameters 37 | ---------- 38 | *exc_types : list of type 39 | """ 40 | self._exc_types = set(exc_types) 41 | 42 | def __enter__(self): 43 | return self 44 | 45 | def __exit__(self, exc_type, exc_val, traceback): 46 | assert exc_type in self._exc_types 47 | self.exc_type = exc_type 48 | self.exc_val = exc_val 49 | self.traceback = traceback 50 | return True 51 | 52 | def __str__(self): 53 | return str(self.exc_val) 54 | 55 | 56 | def normalize_property(prop): 57 | if prop.ndim == 1: 58 | return prop 59 | 60 | n = len(prop) 61 | 62 | arr = numpy.empty(n, dtype='O') 63 | for k in range(n): 64 | arr[k] = prop[k] 65 | 66 | return arr 67 | 68 | 69 | def verify(ply0, ply1): 70 | """ 71 | Verify that two PlyData instances describe the same data. 72 | 73 | Parameters 74 | ---------- 75 | ply0 : PlyData 76 | ply1 : PlyData 77 | 78 | Raises 79 | ------ 80 | AssertionError 81 | """ 82 | el0 = ply0.elements 83 | el1 = ply1.elements 84 | 85 | num_elements = len(el0) 86 | assert len(el1) == num_elements 87 | 88 | for k in range(num_elements): 89 | assert el0[k].name == el1[k].name 90 | 91 | data0 = el0[k].data 92 | data1 = el1[k].data 93 | 94 | dtype0 = el0[k].dtype().descr 95 | dtype1 = el1[k].dtype().descr 96 | 97 | num_properties = len(dtype0) 98 | assert len(dtype1) == num_properties 99 | 100 | for j in range(num_properties): 101 | prop_name = dtype0[j][0] 102 | assert dtype1[j][0] == prop_name 103 | 104 | prop0 = normalize_property(data0[prop_name]) 105 | prop1 = normalize_property(data1[prop_name]) 106 | 107 | verify_1d(prop0, prop1) 108 | 109 | verify_comments(el0[k].comments, el1[k].comments) 110 | 111 | verify_comments(ply0.comments, ply1.comments) 112 | verify_comments(ply0.obj_info, ply1.obj_info) 113 | 114 | 115 | def verify_1d(prop0, prop1): 116 | """ 117 | Verify that two 1-dimensional arrays (possibly of type object) 118 | describe the same data. 119 | """ 120 | n = len(prop0) 121 | assert len(prop1) == n 122 | 123 | s0 = prop0.dtype.descr[0][1][1:] 124 | s1 = prop1.dtype.descr[0][1][1:] 125 | 126 | assert s0 == s1 127 | s = s0[0] 128 | 129 | if s == 'O': 130 | for k in range(n): 131 | assert len(prop0[k]) == len(prop1[k]) 132 | assert (prop0[k] == prop1[k]).all() 133 | else: 134 | assert (prop0 == prop1).all() 135 | 136 | 137 | def verify_comments(comments0, comments1): 138 | """ 139 | Verify that comment lists are identical. 140 | """ 141 | assert len(comments0) == len(comments1) 142 | for (comment0, comment1) in zip(comments0, comments1): 143 | assert comment0 == comment1 144 | 145 | 146 | def write_read(ply, tmpdir, name='test.ply'): 147 | """ 148 | Utility: serialize/deserialize a PlyData instance through a 149 | temporary file. 150 | """ 151 | filename = tmpdir.join(name) 152 | ply.write(str(filename)) 153 | return PlyData.read(str(filename)) 154 | 155 | 156 | def read_str(string, tmpdir, name='test.ply'): 157 | """ 158 | Utility: create a PlyData instance from a string. 159 | """ 160 | filename = tmpdir.join(name) 161 | with filename.open('wb') as f: 162 | f.write(string) 163 | return PlyData.read(str(filename)) 164 | 165 | 166 | def tet_ply(text, byte_order): 167 | vertex = numpy.array([(0, 0, 0), 168 | (0, 1, 1), 169 | (1, 0, 1), 170 | (1, 1, 0)], 171 | dtype=[('x', 'f4'), ('y', 'f4'), ('z', 'f4')]) 172 | 173 | face = numpy.array([([0, 1, 2], 255, 255, 255), 174 | ([0, 2, 3], 255, 0, 0), 175 | ([0, 1, 3], 0, 255, 0), 176 | ([1, 2, 3], 0, 0, 255)], 177 | dtype=[('vertex_indices', 'i4', (3,)), 178 | ('red', 'u1'), ('green', 'u1'), 179 | ('blue', 'u1')]) 180 | 181 | return PlyData( 182 | [ 183 | PlyElement.describe( 184 | vertex, 'vertex', 185 | comments=['tetrahedron vertices'] 186 | ), 187 | PlyElement.describe(face, 'face') 188 | ], 189 | text=text, byte_order=byte_order, 190 | comments=['single tetrahedron with colored faces'] 191 | ) 192 | 193 | 194 | @pytest.fixture(scope='function') 195 | def tet_ply_txt(): 196 | return tet_ply(True, '=') 197 | 198 | 199 | tet_ply_ascii = '''\ 200 | ply\n\ 201 | format ascii 1.0\n\ 202 | comment single tetrahedron with colored faces\n\ 203 | element vertex 4\n\ 204 | comment tetrahedron vertices\n\ 205 | property float x\n\ 206 | property float y\n\ 207 | property float z\n\ 208 | element face 4\n\ 209 | property list uchar int vertex_indices\n\ 210 | property uchar red\n\ 211 | property uchar green\n\ 212 | property uchar blue\n\ 213 | end_header\n\ 214 | 0 0 0\n\ 215 | 0 1 1\n\ 216 | 1 0 1\n\ 217 | 1 1 0\n\ 218 | 3 0 1 2 255 255 255\n\ 219 | 3 0 2 3 255 0 0\n\ 220 | 3 0 1 3 0 255 0\n\ 221 | 3 1 2 3 0 0 255\n\ 222 | '''.encode('ascii') 223 | 224 | np_types = ['i1', 'u1', 'i2', 'u2', 'i4', 'u4', 'f4', 'f8'] 225 | 226 | 227 | @pytest.fixture(scope='function') 228 | def doctest_fixture(tmpdir): 229 | with tmpdir.as_cwd(): 230 | with open('tet.ply', 'wb') as f: 231 | f.write(tet_ply_ascii) 232 | yield 233 | 234 | 235 | def test_doctest_usage(doctest_fixture): 236 | doctest.testfile('doc/usage.md', package=plyfile, 237 | verbose=True, 238 | raise_on_error=True) 239 | 240 | 241 | def test_doctest_faq(doctest_fixture): 242 | doctest.testfile('doc/faq.md', package=plyfile, 243 | verbose=True, 244 | raise_on_error=True) 245 | 246 | 247 | def test_str(tet_ply_txt): 248 | # Nothing to assert; just make sure the call succeeds 249 | str(tet_ply_txt) 250 | 251 | 252 | def test_repr(tet_ply_txt): 253 | # Nothing to assert; just make sure the call succeeds 254 | repr(tet_ply_txt) 255 | 256 | 257 | def test_element_len(tet_ply_txt): 258 | assert len(tet_ply_txt['vertex']) == 4 259 | assert len(tet_ply_txt['face']) == 4 260 | 261 | 262 | def test_element_contains(tet_ply_txt): 263 | assert 'x' in tet_ply_txt['vertex'] 264 | assert 'w' not in tet_ply_txt['vertex'] 265 | 266 | 267 | @pytest.mark.parametrize('np_type', np_types) 268 | def test_property_type(tmpdir, np_type): 269 | dtype = [('x', np_type), ('y', np_type), ('z', np_type)] 270 | a = numpy.array([(1, 2, 3), (4, 5, 6)], dtype=dtype) 271 | 272 | ply0 = PlyData([PlyElement.describe(a, 'test')]) 273 | 274 | assert ply0.elements[0].name == 'test' 275 | assert ply0.elements[0].properties[0].name == 'x' 276 | assert ply0.elements[0].properties[0].val_dtype == np_type 277 | assert ply0.elements[0].properties[1].name == 'y' 278 | assert ply0.elements[0].properties[1].val_dtype == np_type 279 | assert ply0.elements[0].properties[2].name == 'z' 280 | assert ply0.elements[0].properties[2].val_dtype == np_type 281 | 282 | ply1 = write_read(ply0, tmpdir) 283 | 284 | assert ply1.elements[0].name == 'test' 285 | assert ply1.elements[0].data.dtype == dtype 286 | verify(ply0, ply1) 287 | 288 | 289 | @pytest.mark.parametrize('np_type', np_types) 290 | def test_list_property_type(tmpdir, np_type): 291 | a = numpy.array([([0],), ([1, 2, 3],)], dtype=[('x', object)]) 292 | 293 | ply0 = PlyData([PlyElement.describe(a, 'test', 294 | val_types={'x': np_type})]) 295 | 296 | assert ply0.elements[0].name == 'test' 297 | assert ply0.elements[0].properties[0].name == 'x' 298 | assert ply0.elements[0].properties[0].val_dtype == np_type 299 | 300 | ply1 = write_read(ply0, tmpdir) 301 | 302 | assert ply1.elements[0].name == 'test' 303 | assert ply1.elements[0].data[0]['x'].dtype == numpy.dtype(np_type) 304 | verify(ply0, ply1) 305 | 306 | 307 | @pytest.mark.parametrize('len_type', 308 | ['i1', 'u1', 'i2', 'u2', 'i4', 'u4']) 309 | def test_list_property_len_type(tmpdir, len_type): 310 | a = numpy.array([([0],), ([1, 2, 3],)], dtype=[('x', object)]) 311 | 312 | ply0 = PlyData([PlyElement.describe(a, 'test', 313 | len_types={'x': len_type})]) 314 | 315 | assert ply0.elements[0].name == 'test' 316 | assert ply0.elements[0].properties[0].name == 'x' 317 | assert ply0.elements[0].properties[0].val_dtype == 'i4' 318 | assert ply0.elements[0].properties[0].len_dtype == len_type 319 | 320 | ply1 = write_read(ply0, tmpdir) 321 | 322 | assert ply1.elements[0].name == 'test' 323 | assert ply1.elements[0].data[0]['x'].dtype == numpy.dtype('i4') 324 | verify(ply0, ply1) 325 | 326 | 327 | def test_write_stream(tmpdir, tet_ply_txt): 328 | ply0 = tet_ply_txt 329 | test_file = tmpdir.join('test.ply') 330 | 331 | with test_file.open('wb') as f: 332 | tet_ply_txt.write(f) 333 | 334 | ply1 = PlyData.read(str(test_file)) 335 | verify(ply0, ply1) 336 | 337 | 338 | def test_read_stream(tmpdir, tet_ply_txt): 339 | ply0 = tet_ply_txt 340 | test_file = tmpdir.join('test.ply') 341 | 342 | tet_ply_txt.write(str(test_file)) 343 | 344 | with test_file.open('rb') as f: 345 | ply1 = PlyData.read(f) 346 | 347 | verify(ply0, ply1) 348 | 349 | 350 | def test_write_read_str_filename(tmpdir, tet_ply_txt): 351 | ply0 = tet_ply_txt 352 | test_file = tmpdir.join('test.ply') 353 | filename = str(test_file) 354 | 355 | tet_ply_txt.write(filename) 356 | ply1 = PlyData.read(filename) 357 | 358 | verify(ply0, ply1) 359 | 360 | 361 | def test_memmap(tmpdir, tet_ply_txt): 362 | vertex = tet_ply_txt['vertex'] 363 | face0 = PlyElement.describe(tet_ply_txt['face'].data, 'face0') 364 | face1 = PlyElement.describe(tet_ply_txt['face'].data, 'face1') 365 | 366 | # Since the memory mapping requires some manual offset calculation, 367 | # check that it's done correctly when there are elements before 368 | # and after the one that can be memory-mapped. 369 | ply0 = PlyData([face0, vertex, face1]) 370 | ply1 = write_read(ply0, tmpdir) 371 | 372 | assert isinstance(ply1['vertex'].data, numpy.memmap) 373 | 374 | verify(ply0, ply1) 375 | 376 | 377 | def test_copy_on_write(tmpdir, tet_ply_txt): 378 | ply0 = tet_ply_txt 379 | ply0.text = False 380 | filename = str(tmpdir.join('test.ply')) 381 | ply0.write(filename) 382 | ply1 = PlyData.read(filename) 383 | assert isinstance(ply1['vertex'].data, numpy.memmap) 384 | ply1['vertex']['x'] += 1 385 | ply2 = PlyData.read(filename) 386 | 387 | verify(ply0, ply2) 388 | 389 | 390 | def test_memmap_rw(tmpdir, tet_ply_txt): 391 | ply0 = tet_ply_txt 392 | ply0.text = False 393 | filename = str(tmpdir.join('test.ply')) 394 | ply0.write(filename) 395 | with open(filename, 'r+b') as f: 396 | ply1 = PlyData.read(f, mmap='r+') 397 | assert isinstance(ply1['vertex'].data, numpy.memmap) 398 | ply1['vertex']['x'][:] = 100 399 | ply1['vertex'].data.flush() 400 | ply2 = PlyData.read(filename) 401 | 402 | assert (ply2['vertex']['x'] == 100).all() 403 | 404 | 405 | def test_write_invalid_filename(tet_ply_txt): 406 | with Raises(TypeError) as e: 407 | tet_ply_txt.write(None) 408 | 409 | assert str(e) == "expected open file or filename" 410 | 411 | 412 | def test_ascii(tet_ply_txt, tmpdir): 413 | test_file = tmpdir.join('test.ply') 414 | 415 | tet_ply_txt.write(str(test_file)) 416 | assert test_file.read('rb') == tet_ply_ascii 417 | 418 | 419 | @pytest.mark.parametrize('text,byte_order', 420 | [(True, '='), (False, '<'), (False, '>')]) 421 | def test_write_read(tet_ply_txt, tmpdir, text, byte_order): 422 | ply0 = PlyData(tet_ply_txt.elements, text, byte_order, 423 | tet_ply_txt.comments) 424 | ply1 = write_read(ply0, tmpdir) 425 | verify(ply0, ply1) 426 | 427 | 428 | def test_switch_format(tet_ply_txt, tmpdir): 429 | ply0 = tet_ply_txt 430 | ply1 = write_read(ply0, tmpdir, 'test0.ply') 431 | verify(ply0, ply1) 432 | ply1.text = False 433 | ply1.byte_order = '<' 434 | ply2 = write_read(ply1, tmpdir, 'test1.ply') 435 | assert ply2.byte_order == '<' 436 | verify(ply0, ply2) 437 | ply2.byte_order = '>' 438 | ply3 = write_read(ply2, tmpdir, 'test2.ply') 439 | assert ply3.byte_order == '>' 440 | verify(ply0, ply3) 441 | 442 | 443 | def test_invalid_byte_order(tet_ply_txt): 444 | with Raises(ValueError): 445 | tet_ply_txt.byte_order = 'xx' 446 | 447 | 448 | def test_element_lookup(tet_ply_txt): 449 | assert tet_ply_txt['vertex'].name == 'vertex' 450 | assert tet_ply_txt['face'].name == 'face' 451 | 452 | 453 | def test_property_lookup(tet_ply_txt): 454 | vertex = tet_ply_txt['vertex'].data 455 | assert (tet_ply_txt.elements[0]['x'] == vertex['x']).all() 456 | assert (tet_ply_txt.elements[0]['y'] == vertex['y']).all() 457 | assert (tet_ply_txt.elements[0]['z'] == vertex['z']).all() 458 | 459 | face = tet_ply_txt['face'].data 460 | assert (tet_ply_txt.elements[1]['vertex_indices'] == 461 | face['vertex_indices']).all() 462 | assert (tet_ply_txt.elements[1]['red'] == face['red']).all() 463 | assert (tet_ply_txt.elements[1]['green'] == face['green']).all() 464 | assert (tet_ply_txt.elements[1]['blue'] == face['blue']).all() 465 | 466 | 467 | def test_obj_info(tmpdir): 468 | ply0 = PlyData([], text=True, obj_info=['test obj_info']) 469 | test_file = tmpdir.join('test.ply') 470 | ply0.write(str(test_file)) 471 | 472 | ply0_str = test_file.read('rb').decode('ascii') 473 | assert ply0_str.startswith('ply\nformat ascii 1.0\n' 474 | 'obj_info test obj_info\n') 475 | 476 | ply1 = PlyData.read(str(test_file)) 477 | assert len(ply1.obj_info) == 1 478 | assert ply1.obj_info[0] == 'test obj_info' 479 | 480 | 481 | def test_comment_spaces(tmpdir): 482 | ply0 = PlyData([], text=True, comments=[' test comment']) 483 | test_file = tmpdir.join('test.ply') 484 | ply0.write(str(test_file)) 485 | 486 | ply0_str = test_file.read('rb').decode('ascii') 487 | assert ply0_str.startswith('ply\nformat ascii 1.0\n' 488 | 'comment test comment\n') 489 | 490 | ply1 = PlyData.read(str(test_file)) 491 | assert len(ply1.comments) == 1 492 | assert ply1.comments[0] == ' test comment' 493 | 494 | 495 | def test_assign_comments(tet_ply_txt): 496 | ply0 = tet_ply_txt 497 | 498 | ply0.comments = ['comment1', 'comment2'] 499 | ply0.obj_info = ['obj_info1', 'obj_info2'] 500 | verify_comments(ply0.comments, ['comment1', 'comment2']) 501 | verify_comments(ply0.obj_info, ['obj_info1', 'obj_info2']) 502 | 503 | ply0['face'].comments = ['comment1'] 504 | verify_comments(ply0['face'].comments, ['comment1']) 505 | 506 | 507 | def test_assign_comments_newline(tet_ply_txt): 508 | ply0 = tet_ply_txt 509 | 510 | with Raises(ValueError): 511 | ply0.comments = ['comment1\ncomment2'] 512 | 513 | with Raises(ValueError): 514 | ply0.obj_info = ['comment1\ncomment2'] 515 | 516 | with Raises(ValueError): 517 | ply0['face'].comments = ['comment1\ncomment2'] 518 | 519 | 520 | def test_assign_comments_non_ascii(tet_ply_txt): 521 | ply0 = tet_ply_txt 522 | 523 | with Raises(ValueError): 524 | ply0.comments = ['\xb0'] 525 | 526 | with Raises(ValueError): 527 | ply0.obj_info = ['\xb0'] 528 | 529 | with Raises(ValueError): 530 | ply0['face'].comments = ['\xb0'] 531 | 532 | 533 | def test_reorder_elements(tet_ply_txt, tmpdir): 534 | ply0 = tet_ply_txt 535 | (vertex, face) = ply0.elements 536 | ply0.elements = [face, vertex] 537 | 538 | ply1 = write_read(ply0, tmpdir) 539 | 540 | assert ply1.elements[0].name == 'face' 541 | assert ply1.elements[1].name == 'vertex' 542 | 543 | 544 | def test_assign_elements_duplicate(tet_ply_txt): 545 | with Raises(ValueError) as e: 546 | tet_ply_txt.elements = [tet_ply_txt['vertex'], 547 | tet_ply_txt['vertex']] 548 | assert str(e) == "two elements with same name" 549 | 550 | 551 | def test_reorder_properties(tet_ply_txt, tmpdir): 552 | ply0 = tet_ply_txt 553 | vertex = ply0.elements[0] 554 | (x, y, z) = vertex.properties 555 | vertex.properties = [y, z, x] 556 | 557 | ply1 = write_read(ply0, tmpdir) 558 | 559 | assert ply1.elements[0].properties[0].name == 'y' 560 | assert ply1.elements[0].properties[1].name == 'z' 561 | assert ply1.elements[0].properties[2].name == 'x' 562 | 563 | verify_1d(ply0['vertex']['x'], ply1['vertex']['x']) 564 | verify_1d(ply0['vertex']['y'], ply1['vertex']['y']) 565 | verify_1d(ply0['vertex']['z'], ply1['vertex']['z']) 566 | 567 | 568 | @pytest.mark.parametrize('text,byte_order', 569 | [(True, '='), (False, '<'), (False, '>')]) 570 | def test_remove_property(tet_ply_txt, tmpdir, text, byte_order): 571 | ply0 = tet_ply_txt 572 | face = ply0.elements[1] 573 | (vertex_indices, r, g, b) = face.properties 574 | face.properties = [vertex_indices] 575 | 576 | ply0.text = text 577 | ply0.byte_order = byte_order 578 | 579 | ply1 = write_read(ply0, tmpdir) 580 | 581 | assert ply1.text == text 582 | assert ply1.byte_order == byte_order 583 | 584 | assert len(ply1.elements[1].properties) == 1 585 | assert ply1.elements[1].properties[0].name == 'vertex_indices' 586 | 587 | verify_1d(normalize_property(ply1['face']['vertex_indices']), 588 | normalize_property(face['vertex_indices'])) 589 | 590 | 591 | def test_assign_properties_error(tet_ply_txt): 592 | vertex = tet_ply_txt['vertex'] 593 | with Raises(ValueError) as e: 594 | vertex.properties = (vertex.properties + 595 | (PlyProperty('xx', 'i4'),)) 596 | assert str(e) == "dangling property 'xx'" 597 | 598 | 599 | def test_assign_properties_duplicate(tet_ply_txt): 600 | vertex = tet_ply_txt['vertex'] 601 | with Raises(ValueError) as e: 602 | vertex.properties = (vertex.ply_property('x'), 603 | vertex.ply_property('x')) 604 | assert str(e) == "two properties with same name" 605 | 606 | 607 | @pytest.mark.parametrize('text,byte_order', 608 | [(True, '='), (False, '<'), (False, '>')]) 609 | def test_cast_property(tet_ply_txt, tmpdir, text, byte_order): 610 | ply0 = tet_ply_txt 611 | (vertex, face) = ply0.elements 612 | vertex.properties[0].val_dtype = 'f8' 613 | vertex.properties[2].val_dtype = 'u1' 614 | 615 | assert face.properties[0].len_dtype == 'u1' 616 | face.properties[0].len_dtype = 'i4' 617 | 618 | ply0.text = text 619 | ply0.byte_order = byte_order 620 | 621 | ply1 = write_read(ply0, tmpdir) 622 | 623 | assert ply1.text == text 624 | assert ply1.byte_order == byte_order 625 | 626 | assert ply1['vertex']['x'].dtype.descr[0][1][1:] == 'f8' 627 | assert ply1['vertex']['y'].dtype.descr[0][1][1:] == 'f4' 628 | assert ply1['vertex']['z'].dtype.descr[0][1][1:] == 'u1' 629 | 630 | assert (ply1['vertex']['x'] == vertex['x']).all() 631 | assert (ply1['vertex']['y'] == vertex['y']).all() 632 | assert (ply1['vertex']['z'] == vertex['z']).all() 633 | 634 | assert ply1['face'].properties[0].len_dtype == 'i4' 635 | 636 | verify_1d(normalize_property(ply1['face']['vertex_indices']), 637 | normalize_property(face['vertex_indices'])) 638 | 639 | 640 | def test_cast_val_error(tet_ply_txt): 641 | with Raises(ValueError) as e: 642 | tet_ply_txt['vertex'].properties[0].val_dtype = 'xx' 643 | assert str(e).startswith("field type 'xx' not in") 644 | 645 | 646 | def test_cast_len_error(tet_ply_txt): 647 | with Raises(ValueError) as e: 648 | tet_ply_txt['face'].properties[0].len_dtype = 'xx' 649 | assert str(e).startswith("field type 'xx' not in") 650 | 651 | 652 | def ply_abc(fmt, n, data): 653 | string = (b"ply\nformat " + fmt.encode() + b" 1.0\nelement test " + 654 | str(n).encode() + b"\n" 655 | b"property uchar a\nproperty uchar b\n property uchar c\n" 656 | b"end_header\n") 657 | if fmt == 'ascii': 658 | return string + data + b'\n' 659 | else: 660 | return string + data 661 | 662 | 663 | def ply_list_a(fmt, n, data): 664 | string = (b"ply\nformat " + fmt.encode() + b" 1.0\nelement test " + 665 | str(n).encode() + b"\n" 666 | b"property list uchar int a\n" 667 | b"end_header\n") 668 | if fmt == 'ascii': 669 | return string + data + b'\n' 670 | else: 671 | return string + data 672 | 673 | 674 | invalid_cases = [ 675 | (ply_abc('ascii', 1, b'1 2 3.3'), 676 | "row 0: property 'c': malformed input"), 677 | 678 | (ply_list_a('ascii', 1, b''), 679 | "row 0: property 'a': early end-of-line"), 680 | 681 | (ply_list_a('ascii', 1, b'3 2 3'), 682 | "row 0: property 'a': early end-of-line"), 683 | 684 | (ply_abc('ascii', 1, b'1 2 3 4'), 685 | "row 0: expected end-of-line"), 686 | 687 | (ply_abc('ascii', 1, b'1'), 688 | "row 0: property 'b': early end-of-line"), 689 | 690 | (ply_abc('ascii', 2, b'1 2 3'), 691 | "row 1: early end-of-file"), 692 | 693 | (ply_abc('binary_little_endian', 1, b'\x01\x02'), 694 | "row 0: early end-of-file"), 695 | 696 | (ply_list_a('binary_little_endian', 1, b''), 697 | "row 0: property 'a': early end-of-file"), 698 | 699 | (ply_list_a('binary_little_endian', 1, 700 | b'\x03\x01\x00\x00\x00\x02\x00\x00\x00'), 701 | "row 0: property 'a': early end-of-file"), 702 | 703 | (ply_list_a('binary_little_endian', 1, b'\x01\x02'), 704 | "row 0: property 'a': early end-of-file"), 705 | 706 | (ply_abc('binary_little_endian', 2, b'\x01\x02\x03'), 707 | "row 1: early end-of-file") 708 | ] 709 | 710 | 711 | @pytest.mark.parametrize('s,error_string', invalid_cases, 712 | ids=list(map(str, range(len(invalid_cases))))) 713 | def test_invalid(tmpdir, s, error_string): 714 | with Raises(PlyElementParseError) as e: 715 | read_str(s, tmpdir) 716 | assert str(e) == "element 'test': " + error_string 717 | 718 | 719 | def test_assign_elements(tet_ply_txt): 720 | test = PlyElement.describe(numpy.zeros(1, dtype=[('a', 'i4')]), 721 | 'test') 722 | tet_ply_txt.elements = [test] 723 | assert len(tet_ply_txt.elements) == 1 724 | assert len(tet_ply_txt) == 1 725 | assert 'vertex' not in tet_ply_txt 726 | assert 'face' not in tet_ply_txt 727 | assert 'test' in tet_ply_txt 728 | 729 | for (k, elt) in enumerate(tet_ply_txt): 730 | assert elt.name == 'test' 731 | assert k == 0 732 | 733 | 734 | def test_assign_data(tet_ply_txt): 735 | face = tet_ply_txt['face'] 736 | face.data = face.data[:1] 737 | 738 | assert face.count == 1 739 | 740 | 741 | def test_assign_data_error(tet_ply_txt): 742 | vertex = tet_ply_txt['vertex'] 743 | 744 | with Raises(ValueError) as e: 745 | vertex.data = vertex[['x', 'z']] 746 | assert str(e) == "dangling property 'y'" 747 | 748 | 749 | def test_invalid_element_names(): 750 | with Raises(ValueError): 751 | PlyElement.describe(numpy.zeros(1, dtype=[('a', 'i4')]), 752 | '\xb0') 753 | 754 | with Raises(ValueError): 755 | PlyElement.describe(numpy.zeros(1, dtype=[('a', 'i4')]), 756 | 'test test') 757 | 758 | 759 | def test_invalid_property_names(): 760 | with Raises(ValueError): 761 | PlyElement.describe(numpy.zeros(1, dtype=[('\xb0', 'i4')]), 762 | 'test') 763 | 764 | with Raises(ValueError): 765 | PlyElement.describe(numpy.zeros(1, dtype=[('a b', 'i4')]), 766 | 'test') 767 | 768 | 769 | invalid_header_cases = [ 770 | (b'plyy\n', 1), 771 | (b'ply xxx\n', 1), 772 | (b'ply\n\n', 2), 773 | (b'ply\nformat\n', 2), 774 | (b'ply\nelement vertex 0\n', 2), 775 | (b'ply\nformat asciii 1.0\n', 2), 776 | (b'ply\nformat ascii 2.0\n', 2), 777 | (b'ply\nformat ascii 1.0\n', 3), 778 | (b'ply\nformat ascii 1.0\nelement vertex\n', 3), 779 | (b'ply\nformat ascii 1.0\nelement vertex x\n', 3), 780 | (b'ply\nformat ascii 1.0\nelement vertex 0\n' 781 | b'property float\n', 4), 782 | (b'ply\nformat ascii 1.0\nelement vertex 0\n' 783 | b'property list float\n', 4), 784 | (b'ply\nformat ascii 1.0\nelement vertex 0\n' 785 | b'property floatt x\n', 4), 786 | (b'ply\nformat ascii 1.0\nelement vertex 0\n' 787 | b'property float x y\n', 4), 788 | (b'ply\nformat ascii 1.0\nelement vertex 0\n' 789 | b'property list ucharr int extra\n', 4), 790 | (b'ply\nformat ascii 1.0\nelement vertex 0\n' 791 | b'property float x\nend_header xxx\n', 5) 792 | ] 793 | 794 | 795 | @pytest.mark.parametrize( 796 | 's,line', invalid_header_cases, 797 | ids=list(map(str, range(len(invalid_header_cases)))) 798 | ) 799 | def test_header_parse_error(s, line): 800 | with Raises(PlyHeaderParseError) as e: 801 | PlyData.read(BytesIO(s)) 802 | assert e.exc_val.line == line 803 | 804 | 805 | invalid_arrays = [ 806 | numpy.zeros((2, 2)), 807 | numpy.array([(0, (0, 0))], 808 | dtype=[('x', 'f4'), ('y', [('y0', 'f4'), ('y1', 'f4')])]), 809 | numpy.array([(0, (0, 0))], 810 | dtype=[('x', 'f4'), ('y', 'O', (2,))]) 811 | ] 812 | 813 | 814 | @pytest.mark.parametrize( 815 | 'a', invalid_arrays, 816 | ids=list(map(str, range(len(invalid_arrays)))) 817 | ) 818 | def test_invalid_array(a): 819 | with Raises(ValueError): 820 | PlyElement.describe(a, 'test') 821 | 822 | 823 | def test_invalid_array_type(): 824 | with Raises(TypeError): 825 | PlyElement.describe([0, 1, 2], 'test') 826 | 827 | 828 | def test_header_parse_error_repr(): 829 | e = PlyHeaderParseError('text', 11) 830 | assert repr(e) == 'PlyHeaderParseError(\'text\', line=11)' 831 | 832 | 833 | def test_element_parse_error_repr(): 834 | prop = PlyProperty('x', 'f4') 835 | elt = PlyElement('test', [prop], 0) 836 | e = PlyElementParseError('text', elt, 0, prop) 837 | assert repr(e) 838 | 839 | 840 | @pytest.mark.parametrize('text,byte_order', 841 | [(True, '='), (False, '<'), (False, '>')]) 842 | def test_gzip_file(tmpdir, tet_ply_txt, text, byte_order): 843 | ply0 = tet_ply_txt 844 | ply0.text = text 845 | ply0.byte_order = byte_order 846 | test_file = tmpdir.join('test.ply.gz') 847 | 848 | with gzip.open(str(test_file), 'wb') as f: 849 | tet_ply_txt.write(f) 850 | 851 | with gzip.open(str(test_file), 'rb') as f: 852 | ply1 = PlyData.read(f) 853 | 854 | verify(ply0, ply1) 855 | 856 | 857 | @pytest.mark.parametrize('text,byte_order', 858 | [(True, '='), (False, '<'), (False, '>')]) 859 | def test_bytesio(tet_ply_txt, text, byte_order): 860 | ply0 = tet_ply_txt 861 | ply0.text = text 862 | ply0.byte_order = byte_order 863 | fw = BytesIO() 864 | ply0.write(fw) 865 | fr = BytesIO(fw.getvalue()) 866 | ply1 = PlyData.read(fr) 867 | verify(ply0, ply1) 868 | 869 | 870 | def test_mmap_option(tmpdir, tet_ply_txt): 871 | tet_ply_txt.text = False 872 | tet_ply_txt.byte_order = '<' 873 | filename = tmpdir.join('tet.ply') 874 | with filename.open('wb') as f: 875 | tet_ply_txt.write(f) 876 | tet_ply1 = PlyData.read(str(filename)) 877 | assert isinstance(tet_ply1['vertex'].data, numpy.memmap) 878 | tet_ply2 = PlyData.read(str(filename), mmap=False) 879 | assert not isinstance(tet_ply2['vertex'].data, numpy.memmap) 880 | 881 | 882 | def test_read_known_list_len_default(tmpdir, tet_ply_txt): 883 | ply0 = tet_ply_txt 884 | ply0.text = False 885 | ply0.byte_order = '<' 886 | test_file = tmpdir.join('test.ply') 887 | 888 | with test_file.open('wb') as f: 889 | ply0.write(f) 890 | 891 | list_len = {'face': {'vertex_indices': 3}} 892 | ply1 = PlyData.read(str(test_file)) 893 | verify(ply0, ply1) 894 | ply2 = PlyData.read(str(test_file), known_list_len=list_len) 895 | verify(ply0, ply2) 896 | 897 | # test the results of specifying an incorrect length 898 | list_len['face']['vertex_indices'] = 4 899 | with Raises(PlyElementParseError) as e: 900 | PlyData.read(str(test_file), known_list_len=list_len) 901 | assert e.exc_val.message == "early end-of-file" 902 | assert e.exc_val.element.name == 'face' 903 | assert e.exc_val.row == 3 904 | 905 | list_len['face']['vertex_indices'] = 2 906 | with Raises(PlyElementParseError) as e: 907 | PlyData.read(str(test_file), known_list_len=list_len) 908 | assert e.exc_val.message == "unexpected list length" 909 | assert e.exc_val.element.name == 'face' 910 | assert e.exc_val.row == 0 911 | assert e.exc_val.prop.name == 'vertex_indices' 912 | 913 | 914 | def test_read_known_list_len_two_lists_same(tmpdir, tet_ply_txt): 915 | # add another face element to the test data 916 | # so there is a second, identical list "vertex_indices" 917 | ply0 = tet_ply_txt 918 | face2 = numpy.array([([0, 1, 2, 3], 255, 255, 255), 919 | ([0, 2, 3, 1], 255, 0, 0), 920 | ([0, 1, 3, 2], 0, 255, 0)], 921 | dtype=[('vertex_indices2', 'i4', (4,)), 922 | ('red', 'u1'), ('green', 'u1'), 923 | ('blue', 'u1')]) 924 | ply0 = PlyData([ply0['vertex'], ply0['face'], 925 | PlyElement.describe(face2, 'face2')]) 926 | ply0.text = False 927 | ply0.byte_order = '<' 928 | test_file = tmpdir.join('test.ply') 929 | 930 | with test_file.open('wb') as f: 931 | ply0.write(f) 932 | 933 | list_len = {'face': {'vertex_indices': 3}, 934 | 'face2': {'vertex_indices2': 4}} 935 | ply1 = PlyData.read(str(test_file)) 936 | verify(ply0, ply1) 937 | ply2 = PlyData.read(str(test_file), known_list_len=list_len) 938 | verify(ply0, ply2) 939 | 940 | # test the results of specifying an incorrect length 941 | list_len['face']['vertex_indices'] = 4 942 | with Raises(PlyElementParseError) as e: 943 | PlyData.read(str(test_file), known_list_len=list_len) 944 | assert e.exc_val.message == "unexpected list length" 945 | assert e.exc_val.element.name == 'face' 946 | assert e.exc_val.row == 0 947 | assert e.exc_val.prop.name == 'vertex_indices' 948 | 949 | list_len['face']['vertex_indices'] = 2 950 | with Raises(PlyElementParseError) as e: 951 | PlyData.read(str(test_file), known_list_len=list_len) 952 | assert e.exc_val.message == "unexpected list length" 953 | assert e.exc_val.element.name == 'face' 954 | assert e.exc_val.row == 0 955 | assert e.exc_val.prop.name == 'vertex_indices' 956 | 957 | 958 | @pytest.mark.parametrize('text,byte_order', 959 | [(True, '='), (False, '<'), (False, '>')]) 960 | @pytest.mark.parametrize('newline', ['\n', '\r', '\r\n']) 961 | def test_newlines_binary(text, byte_order, newline): 962 | ply0 = tet_ply(text, byte_order) 963 | header = ply0.header + '\n' 964 | header_len = len(header) 965 | 966 | stream0 = BytesIO() 967 | ply0.write(stream0) 968 | ply_str = stream0.getvalue() 969 | data = ply_str[header_len:] 970 | 971 | stream1 = BytesIO() 972 | txt = TextIOWrapper(stream1, 'ascii', newline=newline, 973 | write_through=True) 974 | txt.write(header) 975 | stream1.write(data) 976 | 977 | ply1 = PlyData.read(BytesIO(stream1.getvalue())) 978 | verify(ply0, ply1) 979 | 980 | 981 | def test_text_io(tet_ply_txt): 982 | ply0 = tet_ply_txt 983 | stream0 = StringIO() 984 | ply0.write(stream0) 985 | stream1 = StringIO(stream0.getvalue()) 986 | ply1 = PlyData.read(stream1) 987 | verify(ply0, ply1) 988 | 989 | 990 | def test_text_io_bad_write(tet_ply_txt): 991 | ply0 = tet_ply_txt 992 | ply0.text = False 993 | stream0 = StringIO() 994 | with Raises(ValueError) as e: 995 | ply0.write(stream0) 996 | assert str(e) == "can't write binary-format PLY to text stream" 997 | 998 | 999 | def test_text_io_bad_read(tet_ply_txt): 1000 | # Use valid ASCII bytes 1001 | vertex_data = numpy.array( 1002 | [(64, 64, 64), (64, 64, 64)], 1003 | dtype=[('x', 'u1'), ('y', 'u1'), ('z', 'u1')]) 1004 | elt = PlyElement.describe(vertex_data, 'vertex') 1005 | ply0 = PlyData([elt], text=False) 1006 | stream0 = BytesIO() 1007 | ply0.write(stream0) 1008 | stream1 = TextIOWrapper(BytesIO(stream0.getvalue()), 'ascii') 1009 | with Raises(ValueError) as e: 1010 | PlyData.read(stream1) 1011 | assert str(e) == "can't read binary-format PLY from text stream" 1012 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /plyfile.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014-2025 Darsh Ranjan and plyfile authors. 2 | # 3 | # This file is part of python-plyfile. 4 | # 5 | # python-plyfile is free software: you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # python-plyfile is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with python-plyfile. If not, see 17 | # . 18 | """ 19 | NumPy-based PLY format input and output for Python. 20 | """ 21 | 22 | import io as _io 23 | from itertools import islice as _islice 24 | 25 | import numpy as _np 26 | from sys import byteorder as _byteorder 27 | 28 | 29 | class PlyData(object): 30 | """ 31 | PLY file header and data. 32 | 33 | A `PlyData` instance is created in one of two ways: by the static 34 | method `PlyData.read` (to read a PLY file), or directly from 35 | `__init__` given a sequence of elements (which can then be written 36 | to a PLY file). 37 | 38 | Attributes 39 | ---------- 40 | elements : list of PlyElement 41 | comments : list of str 42 | obj_info : list of str 43 | text : bool 44 | byte_order : {'<', '>', '='} 45 | header : str 46 | """ 47 | 48 | def __init__(self, elements=[], text=False, byte_order='=', 49 | comments=[], obj_info=[]): 50 | """ 51 | Parameters 52 | ---------- 53 | elements : iterable of PlyElement 54 | text : bool, optional 55 | Whether the resulting PLY file will be text (True) or 56 | binary (False). 57 | byte_order : {'<', '>', '='}, optional 58 | `'<'` for little-endian, `'>'` for big-endian, or `'='` 59 | for native. This is only relevant if `text` is False. 60 | comments : iterable of str, optional 61 | Comment lines between "ply" and "format" lines. 62 | obj_info : iterable of str, optional 63 | like comments, but will be placed in the header with 64 | "obj_info ..." instead of "comment ...". 65 | """ 66 | self.byte_order = byte_order 67 | self.text = text 68 | 69 | self.comments = comments 70 | self.obj_info = obj_info 71 | self.elements = elements 72 | 73 | def _get_elements(self): 74 | return self._elements 75 | 76 | def _set_elements(self, elements): 77 | self._elements = tuple(elements) 78 | self._index() 79 | 80 | elements = property(_get_elements, _set_elements) 81 | 82 | def _get_byte_order(self): 83 | if not self.text and self._byte_order == '=': 84 | return _native_byte_order 85 | return self._byte_order 86 | 87 | def _set_byte_order(self, byte_order): 88 | if byte_order not in ['<', '>', '=']: 89 | raise ValueError("byte order must be '<', '>', or '='") 90 | 91 | self._byte_order = byte_order 92 | 93 | byte_order = property(_get_byte_order, _set_byte_order) 94 | 95 | def _index(self): 96 | self._element_lookup = dict((elt.name, elt) for elt in 97 | self._elements) 98 | if len(self._element_lookup) != len(self._elements): 99 | raise ValueError("two elements with same name") 100 | 101 | def _get_comments(self): 102 | return list(self._comments) 103 | 104 | def _set_comments(self, comments): 105 | _check_comments(comments) 106 | self._comments = list(comments) 107 | 108 | comments = property(_get_comments, _set_comments) 109 | 110 | def _get_obj_info(self): 111 | return list(self._obj_info) 112 | 113 | def _set_obj_info(self, obj_info): 114 | _check_comments(obj_info) 115 | self._obj_info = list(obj_info) 116 | 117 | obj_info = property(_get_obj_info, _set_obj_info) 118 | 119 | @staticmethod 120 | def _parse_header(stream): 121 | parser = _PlyHeaderParser(_PlyHeaderLines(stream)) 122 | return PlyData( 123 | [PlyElement(*e) for e in parser.elements], 124 | parser.format == 'ascii', 125 | _byte_order_map[parser.format], 126 | parser.comments, 127 | parser.obj_info 128 | ) 129 | 130 | @staticmethod 131 | def read(stream, mmap='c', known_list_len={}): 132 | """ 133 | Read PLY data from a readable file-like object or filename. 134 | 135 | Parameters 136 | ---------- 137 | stream : str or readable open file 138 | mmap : {'c', 'r', 'r+'} or bool, optional (default='c') 139 | Configures memory-mapping. Any falsy value disables 140 | memory mapping, and any non-string truthy value is 141 | equivalent to 'c', for copy-on-write mapping. 142 | known_list_len : dict, optional 143 | Mapping from element names to mappings from list property 144 | names to their fixed lengths. This optional argument is 145 | necessary to enable memory mapping of elements that contain 146 | list properties. (Note that elements with variable-length 147 | list properties cannot be memory-mapped.) 148 | 149 | Raises 150 | ------ 151 | PlyParseError 152 | If the file cannot be parsed for any reason. 153 | ValueError 154 | If `stream` is open in text mode but the PLY header 155 | indicates binary encoding. 156 | """ 157 | (must_close, stream) = _open_stream(stream, 'read') 158 | try: 159 | data = PlyData._parse_header(stream) 160 | if isinstance(stream.read(0), str): 161 | if data.text: 162 | data_stream = stream 163 | else: 164 | raise ValueError("can't read binary-format PLY " 165 | "from text stream") 166 | else: 167 | if data.text: 168 | data_stream = _io.TextIOWrapper(stream, 'ascii') 169 | else: 170 | data_stream = stream 171 | for elt in data: 172 | elt._read(data_stream, data.text, data.byte_order, mmap, 173 | known_list_len=known_list_len.get(elt.name, {})) 174 | finally: 175 | if must_close: 176 | stream.close() 177 | 178 | return data 179 | 180 | def write(self, stream): 181 | """ 182 | Write PLY data to a writeable file-like object or filename. 183 | 184 | Parameters 185 | ---------- 186 | stream : str or writeable open file 187 | 188 | Raises 189 | ------ 190 | ValueError 191 | If `stream` is open in text mode and the file to be written 192 | is binary-format. 193 | """ 194 | (must_close, stream) = _open_stream(stream, 'write') 195 | try: 196 | try: 197 | stream.write(b'') 198 | binary_stream = True 199 | except TypeError: 200 | binary_stream = False 201 | if binary_stream: 202 | stream.write(self.header.encode('ascii')) 203 | stream.write(b'\n') 204 | else: 205 | if not self.text: 206 | raise ValueError("can't write binary-format PLY to " 207 | "text stream") 208 | stream.write(self.header) 209 | stream.write('\n') 210 | for elt in self: 211 | elt._write(stream, self.text, self.byte_order) 212 | finally: 213 | if must_close: 214 | stream.close() 215 | 216 | @property 217 | def header(self): 218 | """ 219 | PLY-formatted metadata for the instance. 220 | """ 221 | lines = ['ply'] 222 | 223 | if self.text: 224 | lines.append('format ascii 1.0') 225 | else: 226 | lines.append('format ' + 227 | _byte_order_reverse[self.byte_order] + 228 | ' 1.0') 229 | 230 | # Some information is lost here, since all comments are placed 231 | # between the 'format' line and the first element. 232 | for c in self.comments: 233 | lines.append('comment ' + c) 234 | 235 | for c in self.obj_info: 236 | lines.append('obj_info ' + c) 237 | 238 | lines.extend(elt.header for elt in self.elements) 239 | lines.append('end_header') 240 | return '\n'.join(lines) 241 | 242 | def __iter__(self): 243 | """ 244 | Iterate over the elements. 245 | """ 246 | return iter(self.elements) 247 | 248 | def __len__(self): 249 | """ 250 | Return the number of elements. 251 | """ 252 | return len(self.elements) 253 | 254 | def __contains__(self, name): 255 | """ 256 | Check if an element with the given name exists. 257 | """ 258 | return name in self._element_lookup 259 | 260 | def __getitem__(self, name): 261 | """ 262 | Retrieve an element by name. 263 | 264 | Parameters 265 | ---------- 266 | name : str 267 | 268 | Returns 269 | ------- 270 | PlyElement 271 | 272 | Raises 273 | ------ 274 | KeyError 275 | If the element can't be found. 276 | """ 277 | return self._element_lookup[name] 278 | 279 | def __str__(self): 280 | return self.header 281 | 282 | def __repr__(self): 283 | return ('PlyData(%r, text=%r, byte_order=%r, ' 284 | 'comments=%r, obj_info=%r)' % 285 | (self.elements, self.text, self.byte_order, 286 | self.comments, self.obj_info)) 287 | 288 | 289 | class PlyElement(object): 290 | """ 291 | PLY file element. 292 | 293 | Creating a `PlyElement` instance is generally done in one of two 294 | ways: as a byproduct of `PlyData.read` (when reading a PLY file) and 295 | by `PlyElement.describe` (before writing a PLY file). 296 | 297 | Attributes 298 | ---------- 299 | name : str 300 | count : int 301 | data : numpy.ndarray 302 | properties : list of PlyProperty 303 | comments : list of str 304 | header : str 305 | PLY header block for this element. 306 | """ 307 | 308 | def __init__(self, name, properties, count, comments=[]): 309 | """ 310 | This is not part of the public interface. The preferred methods 311 | of obtaining `PlyElement` instances are `PlyData.read` (to read 312 | from a file) and `PlyElement.describe` (to construct from a 313 | `numpy` array). 314 | 315 | Parameters 316 | ---------- 317 | name : str 318 | properties : list of PlyProperty 319 | count : str 320 | comments : list of str 321 | """ 322 | _check_name(name) 323 | self._name = str(name) 324 | self._count = count 325 | 326 | self._properties = tuple(properties) 327 | self._index() 328 | 329 | self.comments = comments 330 | 331 | self._have_list = any(isinstance(p, PlyListProperty) 332 | for p in self.properties) 333 | 334 | @property 335 | def count(self): 336 | return self._count 337 | 338 | def _get_data(self): 339 | return self._data 340 | 341 | def _set_data(self, data): 342 | self._data = data 343 | self._count = len(data) 344 | self._check_sanity() 345 | 346 | data = property(_get_data, _set_data) 347 | 348 | def _check_sanity(self): 349 | for prop in self.properties: 350 | if prop.name not in self._data.dtype.fields: 351 | raise ValueError("dangling property %r" % prop.name) 352 | 353 | def _get_properties(self): 354 | return self._properties 355 | 356 | def _set_properties(self, properties): 357 | self._properties = tuple(properties) 358 | self._check_sanity() 359 | self._index() 360 | 361 | properties = property(_get_properties, _set_properties) 362 | 363 | def _get_comments(self): 364 | return list(self._comments) 365 | 366 | def _set_comments(self, comments): 367 | _check_comments(comments) 368 | self._comments = list(comments) 369 | 370 | comments = property(_get_comments, _set_comments) 371 | 372 | def _index(self): 373 | self._property_lookup = dict((prop.name, prop) 374 | for prop in self._properties) 375 | if len(self._property_lookup) != len(self._properties): 376 | raise ValueError("two properties with same name") 377 | 378 | def ply_property(self, name): 379 | """ 380 | Look up property by name. 381 | 382 | Parameters 383 | ---------- 384 | name : str 385 | 386 | Returns 387 | ------- 388 | PlyProperty 389 | 390 | Raises 391 | ------ 392 | KeyError 393 | If the property can't be found. 394 | """ 395 | return self._property_lookup[name] 396 | 397 | @property 398 | def name(self): 399 | return self._name 400 | 401 | def dtype(self, byte_order='='): 402 | """ 403 | Return the `numpy.dtype` description of the in-memory 404 | representation of the data. (If there are no list properties, 405 | and the PLY format is binary, then this also accurately 406 | describes the on-disk representation of the element.) 407 | 408 | Parameters 409 | ---------- 410 | byte_order : {'<', '>', '='} 411 | 412 | Returns 413 | ------- 414 | numpy.dtype 415 | """ 416 | return _np.dtype([(prop.name, prop.dtype(byte_order)) 417 | for prop in self.properties]) 418 | 419 | @staticmethod 420 | def describe(data, name, len_types={}, val_types={}, 421 | comments=[]): 422 | """ 423 | Construct a `PlyElement` instance from an array's metadata. 424 | 425 | Parameters 426 | ---------- 427 | data : numpy.ndarray 428 | Structured `numpy` array. 429 | len_types : dict, optional 430 | Mapping from list property names to type strings 431 | (`numpy`-style like `'u1'`, `'f4'`, etc., or PLY-style like 432 | `'int8'`, `'float32'`, etc.), which will be used to encode 433 | the length of the list in binary-format PLY files. Defaults 434 | to `'u1'` (8-bit integer) for all list properties. 435 | val_types : dict, optional 436 | Mapping from list property names to type strings as for 437 | `len_types`, but is used to encode the list elements in 438 | binary-format PLY files. Defaults to `'i4'` (32-bit 439 | integer) for all list properties. 440 | comments : list of str 441 | Comments between the "element" line and first property 442 | definition in the header. 443 | 444 | Returns 445 | ------- 446 | PlyElement 447 | 448 | Raises 449 | ------ 450 | TypeError, ValueError 451 | """ 452 | if not isinstance(data, _np.ndarray): 453 | raise TypeError("only numpy arrays are supported") 454 | 455 | if len(data.shape) != 1: 456 | raise ValueError("only one-dimensional arrays are " 457 | "supported") 458 | 459 | count = len(data) 460 | 461 | properties = [] 462 | descr = data.dtype.descr 463 | 464 | for t in descr: 465 | if not isinstance(t[1], str): 466 | raise ValueError("nested records not supported") 467 | 468 | if not t[0]: 469 | raise ValueError("field with empty name") 470 | 471 | if len(t) != 2 or t[1][1] == 'O': 472 | # non-scalar field, which corresponds to a list 473 | # property in PLY. 474 | 475 | if t[1][1] == 'O': 476 | if len(t) != 2: 477 | raise ValueError("non-scalar object fields not " 478 | "supported") 479 | 480 | len_str = _data_type_reverse[len_types.get(t[0], 'u1')] 481 | if t[1][1] == 'O': 482 | val_type = val_types.get(t[0], 'i4') 483 | val_str = _lookup_type(val_type) 484 | else: 485 | val_str = _lookup_type(t[1][1:]) 486 | 487 | prop = PlyListProperty(t[0], len_str, val_str) 488 | else: 489 | val_str = _lookup_type(t[1][1:]) 490 | prop = PlyProperty(t[0], val_str) 491 | 492 | properties.append(prop) 493 | 494 | elt = PlyElement(name, properties, count, comments) 495 | elt.data = data 496 | 497 | return elt 498 | 499 | def _read(self, stream, text, byte_order, mmap, 500 | known_list_len={}): 501 | """ 502 | Read the actual data from a PLY file. 503 | 504 | Parameters 505 | ---------- 506 | stream : readable open file 507 | text : bool 508 | byte_order : {'<', '>', '='} 509 | mmap : {'c', 'r', 'r+'} or bool 510 | known_list_len : dict 511 | """ 512 | if text: 513 | self._read_txt(stream) 514 | else: 515 | list_prop_names = set(p.name for p in self.properties 516 | if isinstance(p, PlyListProperty)) 517 | can_mmap_lists = list_prop_names <= set(known_list_len) 518 | if mmap and _can_mmap(stream) and can_mmap_lists: 519 | # Loading the data is straightforward. We will memory 520 | # map the file in copy-on-write mode. 521 | mmap_mode = mmap if isinstance(mmap, str) else 'c' 522 | self._read_mmap(stream, byte_order, mmap_mode, 523 | known_list_len) 524 | else: 525 | # A simple load is impossible. 526 | self._read_bin(stream, byte_order) 527 | 528 | self._check_sanity() 529 | 530 | def _write(self, stream, text, byte_order): 531 | """ 532 | Write the data to a PLY file. 533 | 534 | Parameters 535 | ---------- 536 | stream : writeable open file 537 | text : bool 538 | byte_order : {'<', '>', '='} 539 | """ 540 | if text: 541 | self._write_txt(stream) 542 | else: 543 | if self._have_list: 544 | # There are list properties, so serialization is 545 | # slightly complicated. 546 | self._write_bin(stream, byte_order) 547 | else: 548 | # no list properties, so serialization is 549 | # straightforward. 550 | stream.write(self.data.astype(self.dtype(byte_order), 551 | copy=False).data) 552 | 553 | def _read_mmap(self, stream, byte_order, mmap_mode, known_list_len): 554 | """ 555 | Memory-map an input file as `self.data`. 556 | 557 | Parameters 558 | ---------- 559 | stream : readable open file 560 | byte_order : {'<', '>', '='} 561 | mmap_mode: str 562 | known_list_len : dict 563 | """ 564 | list_len_props = {} 565 | # update the dtype to include the list length and list dtype 566 | new_dtype = [] 567 | for p in self.properties: 568 | if isinstance(p, PlyListProperty): 569 | len_dtype, val_dtype = p.list_dtype(byte_order) 570 | # create new dtype for the list length 571 | new_dtype.append((p.name + "\nlen", len_dtype)) 572 | # a new dtype with size for the list values themselves 573 | new_dtype.append((p.name, val_dtype, 574 | (known_list_len[p.name],))) 575 | list_len_props[p.name] = p.name + "\nlen" 576 | else: 577 | new_dtype.append((p.name, p.dtype(byte_order))) 578 | dtype = _np.dtype(new_dtype) 579 | num_bytes = self.count * dtype.itemsize 580 | offset = stream.tell() 581 | stream.seek(0, 2) 582 | max_bytes = stream.tell() - offset 583 | if max_bytes < num_bytes: 584 | raise PlyElementParseError("early end-of-file", self, 585 | max_bytes // dtype.itemsize) 586 | self._data = _np.memmap(stream, dtype, mmap_mode, offset, self.count) 587 | # Fix stream position 588 | stream.seek(offset + self.count * dtype.itemsize) 589 | # remove any extra properties added 590 | for prop in list_len_props: 591 | field = list_len_props[prop] 592 | len_check = self._data[field] == known_list_len[prop] 593 | if not len_check.all(): 594 | row = _np.flatnonzero(len_check ^ True)[0] 595 | raise PlyElementParseError( 596 | "unexpected list length", 597 | self, row, self.ply_property(prop)) 598 | props = [p.name for p in self.properties] 599 | self._data = self._data[props] 600 | 601 | def _read_txt(self, stream): 602 | """ 603 | Load a PLY element from an ASCII-format PLY file. The element 604 | may contain list properties. 605 | 606 | Parameters 607 | ---------- 608 | stream : readable open file 609 | """ 610 | self._data = _np.empty(self.count, dtype=self.dtype()) 611 | 612 | k = 0 613 | for line in _islice(iter(stream.readline, ''), self.count): 614 | fields = iter(line.strip().split()) 615 | for prop in self.properties: 616 | try: 617 | self._data[prop.name][k] = prop._from_fields(fields) 618 | except StopIteration: 619 | raise PlyElementParseError("early end-of-line", 620 | self, k, prop) 621 | except ValueError: 622 | raise PlyElementParseError("malformed input", 623 | self, k, prop) 624 | try: 625 | next(fields) 626 | except StopIteration: 627 | pass 628 | else: 629 | raise PlyElementParseError("expected end-of-line", 630 | self, k) 631 | k += 1 632 | 633 | if k < self.count: 634 | del self._data 635 | raise PlyElementParseError("early end-of-file", self, k) 636 | 637 | def _write_txt(self, stream): 638 | """ 639 | Save a PLY element to an ASCII-format PLY file. The element may 640 | contain list properties. 641 | 642 | Parameters 643 | ---------- 644 | stream : writeable open file 645 | """ 646 | for rec in self.data: 647 | fields = [] 648 | for prop in self.properties: 649 | fields.extend(prop._to_fields(rec[prop.name])) 650 | 651 | _np.savetxt(stream, [fields], '%.18g', newline='\n') 652 | 653 | def _read_bin(self, stream, byte_order): 654 | """ 655 | Load a PLY element from a binary PLY file. The element may 656 | contain list properties. 657 | 658 | Parameters 659 | ---------- 660 | stream : readable open file 661 | byte_order : {'<', '>', '='} 662 | """ 663 | self._data = _np.empty(self.count, dtype=self.dtype(byte_order)) 664 | 665 | for k in range(self.count): 666 | for prop in self.properties: 667 | try: 668 | self._data[prop.name][k] = \ 669 | prop._read_bin(stream, byte_order) 670 | except StopIteration: 671 | raise PlyElementParseError("early end-of-file", 672 | self, k, prop) 673 | 674 | def _write_bin(self, stream, byte_order): 675 | """ 676 | Save a PLY element to a binary PLY file. The element may 677 | contain list properties. 678 | 679 | Parameters 680 | ---------- 681 | stream : writeable open file 682 | byte_order : {'<', '>', '='} 683 | """ 684 | for rec in self.data: 685 | for prop in self.properties: 686 | prop._write_bin(rec[prop.name], stream, byte_order) 687 | 688 | @property 689 | def header(self): 690 | lines = ['element %s %d' % (self.name, self.count)] 691 | 692 | # Some information is lost here, since all comments are placed 693 | # between the 'element' line and the first property definition. 694 | for c in self.comments: 695 | lines.append('comment ' + c) 696 | 697 | lines.extend(list(map(str, self.properties))) 698 | 699 | return '\n'.join(lines) 700 | 701 | def __len__(self): 702 | """ 703 | Return the number of rows in the element. 704 | """ 705 | return self.count 706 | 707 | def __contains__(self, name): 708 | """ 709 | Determine if a property with the given name exists. 710 | """ 711 | return name in self._property_lookup 712 | 713 | def __getitem__(self, key): 714 | """ 715 | Proxy to `self.data.__getitem__` for convenience. 716 | """ 717 | return self.data[key] 718 | 719 | def __setitem__(self, key, value): 720 | """ 721 | Proxy to `self.data.__setitem__` for convenience. 722 | """ 723 | self.data[key] = value 724 | 725 | def __str__(self): 726 | return self.header 727 | 728 | def __repr__(self): 729 | return ('PlyElement(%r, %r, count=%d, comments=%r)' % 730 | (self.name, self.properties, self.count, 731 | self.comments)) 732 | 733 | 734 | class PlyProperty(object): 735 | """ 736 | PLY property description. 737 | 738 | This class is pure metadata. The data itself is contained in 739 | `PlyElement` instances. 740 | 741 | Attributes 742 | ---------- 743 | name : str 744 | val_dtype : str 745 | `numpy.dtype` description for the property's data. 746 | """ 747 | 748 | def __init__(self, name, val_dtype): 749 | """ 750 | Parameters 751 | ---------- 752 | name : str 753 | val_dtype : str 754 | """ 755 | _check_name(name) 756 | self._name = str(name) 757 | self.val_dtype = val_dtype 758 | 759 | def _get_val_dtype(self): 760 | return self._val_dtype 761 | 762 | def _set_val_dtype(self, val_dtype): 763 | self._val_dtype = _data_types[_lookup_type(val_dtype)] 764 | 765 | val_dtype = property(_get_val_dtype, _set_val_dtype) 766 | 767 | @property 768 | def name(self): 769 | return self._name 770 | 771 | def dtype(self, byte_order='='): 772 | """ 773 | Return the `numpy.dtype` description for this property. 774 | 775 | Parameters 776 | ---------- 777 | byte_order : {'<', '>', '='}, default='=' 778 | 779 | Returns 780 | ------- 781 | tuple of str 782 | """ 783 | return byte_order + self.val_dtype 784 | 785 | def _from_fields(self, fields): 786 | """ 787 | Parse data from generator. 788 | 789 | Parameters 790 | ---------- 791 | fields : iterator of str 792 | 793 | Returns 794 | ------- 795 | data 796 | Parsed data of the correct type. 797 | 798 | Raises 799 | ------ 800 | StopIteration 801 | if the property's data could not be read. 802 | """ 803 | return _np.dtype(self.dtype()).type(next(fields)) 804 | 805 | def _to_fields(self, data): 806 | """ 807 | Parameters 808 | ---------- 809 | data 810 | Property data to encode. 811 | 812 | Yields 813 | ------ 814 | encoded_data 815 | Data with type consistent with `self.val_dtype`. 816 | """ 817 | yield _np.dtype(self.dtype()).type(data) 818 | 819 | def _read_bin(self, stream, byte_order): 820 | """ 821 | Read data from a binary stream. 822 | 823 | Parameters 824 | ---------- 825 | stream : readable open binary file 826 | byte_order : {'<'. '>', '='} 827 | 828 | Raises 829 | ------ 830 | StopIteration 831 | If the property data could not be read. 832 | """ 833 | try: 834 | return _read_array(stream, self.dtype(byte_order), 1)[0] 835 | except IndexError: 836 | raise StopIteration 837 | 838 | def _write_bin(self, data, stream, byte_order): 839 | """ 840 | Write data to a binary stream. 841 | 842 | Parameters 843 | ---------- 844 | data 845 | Property data to encode. 846 | stream : writeable open binary file 847 | byte_order : {'<', '>', '='} 848 | """ 849 | _write_array(stream, _np.dtype(self.dtype(byte_order)).type(data)) 850 | 851 | def __str__(self): 852 | val_str = _data_type_reverse[self.val_dtype] 853 | return 'property %s %s' % (val_str, self.name) 854 | 855 | def __repr__(self): 856 | return 'PlyProperty(%r, %r)' % (self.name, 857 | _lookup_type(self.val_dtype)) 858 | 859 | 860 | class PlyListProperty(PlyProperty): 861 | """ 862 | PLY list property description. 863 | 864 | Attributes 865 | ---------- 866 | name 867 | val_dtype 868 | len_dtype : str 869 | `numpy.dtype` description for the property's length field. 870 | """ 871 | 872 | def __init__(self, name, len_dtype, val_dtype): 873 | """ 874 | Parameters 875 | ---------- 876 | name : str 877 | len_dtype : str 878 | val_dtype : str 879 | """ 880 | PlyProperty.__init__(self, name, val_dtype) 881 | 882 | self.len_dtype = len_dtype 883 | 884 | def _get_len_dtype(self): 885 | return self._len_dtype 886 | 887 | def _set_len_dtype(self, len_dtype): 888 | self._len_dtype = _data_types[_lookup_type(len_dtype)] 889 | 890 | len_dtype = property(_get_len_dtype, _set_len_dtype) 891 | 892 | def dtype(self, byte_order='='): 893 | """ 894 | `numpy.dtype` name for the property's field in the element. 895 | 896 | List properties always have a numpy dtype of "object". 897 | 898 | Parameters 899 | ---------- 900 | byte_order : {'<', '>', '='} 901 | 902 | Returns 903 | ------- 904 | dtype : str 905 | Always `'|O'`. 906 | """ 907 | return '|O' 908 | 909 | def list_dtype(self, byte_order='='): 910 | """ 911 | Return the pair `(len_dtype, val_dtype)` (both numpy-friendly 912 | strings). 913 | 914 | Parameters 915 | ---------- 916 | byte_order : {'<', '>', '='} 917 | 918 | Returns 919 | ------- 920 | len_dtype : str 921 | val_dtype : str 922 | """ 923 | return (byte_order + self.len_dtype, 924 | byte_order + self.val_dtype) 925 | 926 | def _from_fields(self, fields): 927 | """ 928 | Parse data from generator. 929 | 930 | Parameters 931 | ---------- 932 | fields : iterator of str 933 | 934 | Returns 935 | ------- 936 | data : numpy.ndarray 937 | Parsed list data for the property. 938 | 939 | Raises 940 | ------ 941 | StopIteration 942 | if the property's data could not be read. 943 | """ 944 | (len_t, val_t) = self.list_dtype() 945 | 946 | n = int(_np.dtype(len_t).type(next(fields))) 947 | 948 | data = _np.loadtxt(list(_islice(fields, n)), val_t, ndmin=1) 949 | if len(data) < n: 950 | raise StopIteration 951 | 952 | return data 953 | 954 | def _to_fields(self, data): 955 | """ 956 | Return generator over the (numerical) PLY representation of the 957 | list data (length followed by actual data). 958 | 959 | Parameters 960 | ---------- 961 | data : numpy.ndarray 962 | Property data to encode. 963 | 964 | Yields 965 | ------ 966 | Length followed by each list element. 967 | """ 968 | (len_t, val_t) = self.list_dtype() 969 | 970 | data = _np.asarray(data, dtype=val_t).ravel() 971 | 972 | yield _np.dtype(len_t).type(data.size) 973 | for x in data: 974 | yield x 975 | 976 | def _read_bin(self, stream, byte_order): 977 | """ 978 | Read data from a binary stream. 979 | 980 | Parameters 981 | ---------- 982 | stream : readable open binary file 983 | byte_order : {'<', '>', '='} 984 | 985 | Returns 986 | ------- 987 | data : numpy.ndarray 988 | 989 | Raises 990 | ------ 991 | StopIteration 992 | If data could not be read. 993 | """ 994 | (len_t, val_t) = self.list_dtype(byte_order) 995 | 996 | try: 997 | n = _read_array(stream, _np.dtype(len_t), 1)[0] 998 | except IndexError: 999 | raise StopIteration 1000 | 1001 | data = _read_array(stream, _np.dtype(val_t), n) 1002 | if len(data) < n: 1003 | raise StopIteration 1004 | 1005 | return data 1006 | 1007 | def _write_bin(self, data, stream, byte_order): 1008 | """ 1009 | Write data to a binary stream. 1010 | 1011 | Parameters 1012 | ---------- 1013 | data : numpy.ndarray 1014 | Data to encode. 1015 | stream : writeable open binary file 1016 | byte_order : {'<', '>', '='} 1017 | """ 1018 | (len_t, val_t) = self.list_dtype(byte_order) 1019 | 1020 | data = _np.asarray(data, dtype=val_t).ravel() 1021 | 1022 | _write_array(stream, _np.array(data.size, dtype=len_t)) 1023 | _write_array(stream, data) 1024 | 1025 | def __str__(self): 1026 | len_str = _data_type_reverse[self.len_dtype] 1027 | val_str = _data_type_reverse[self.val_dtype] 1028 | return 'property list %s %s %s' % (len_str, val_str, self.name) 1029 | 1030 | def __repr__(self): 1031 | return ('PlyListProperty(%r, %r, %r)' % 1032 | (self.name, 1033 | _lookup_type(self.len_dtype), 1034 | _lookup_type(self.val_dtype))) 1035 | 1036 | 1037 | class PlyParseError(Exception): 1038 | """ 1039 | Base class for PLY parsing errors. 1040 | """ 1041 | 1042 | pass 1043 | 1044 | 1045 | class PlyElementParseError(PlyParseError): 1046 | """ 1047 | Raised when a PLY element cannot be parsed. 1048 | 1049 | Attributes 1050 | ---------- 1051 | message : str 1052 | element : PlyElement 1053 | row : int 1054 | prop : PlyProperty 1055 | """ 1056 | 1057 | def __init__(self, message, element=None, row=None, prop=None): 1058 | self.message = message 1059 | self.element = element 1060 | self.row = row 1061 | self.prop = prop 1062 | 1063 | s = '' 1064 | if self.element: 1065 | s += 'element %r: ' % self.element.name 1066 | if self.row is not None: 1067 | s += 'row %d: ' % self.row 1068 | if self.prop: 1069 | s += 'property %r: ' % self.prop.name 1070 | s += self.message 1071 | 1072 | Exception.__init__(self, s) 1073 | 1074 | def __repr__(self): 1075 | return ('%s(%r, element=%r, row=%r, prop=%r)' % 1076 | (self.__class__.__name__, 1077 | self.message, self.element, self.row, self.prop)) 1078 | 1079 | 1080 | class PlyHeaderParseError(PlyParseError): 1081 | """ 1082 | Raised when a PLY header cannot be parsed. 1083 | 1084 | Attributes 1085 | ---------- 1086 | line : str 1087 | Which header line the error occurred on. 1088 | """ 1089 | 1090 | def __init__(self, message, line=None): 1091 | self.message = message 1092 | self.line = line 1093 | 1094 | s = '' 1095 | if self.line: 1096 | s += 'line %r: ' % self.line 1097 | s += self.message 1098 | 1099 | Exception.__init__(self, s) 1100 | 1101 | def __repr__(self): 1102 | return ('%s(%r, line=%r)' % 1103 | (self.__class__.__name__, 1104 | self.message, self.line)) 1105 | 1106 | 1107 | class _PlyHeaderParser(object): 1108 | """ 1109 | Parser for PLY format header. 1110 | 1111 | Attributes 1112 | ---------- 1113 | format : str 1114 | "ascii", "binary_little_endian", or "binary_big_endian" 1115 | elements : list of (name, comments, count, properties) 1116 | comments : list of str 1117 | obj_info : list of str 1118 | lines : int 1119 | """ 1120 | 1121 | def __init__(self, lines): 1122 | """ 1123 | Parameters 1124 | ---------- 1125 | lines : iterable of str 1126 | Header lines, starting *after* the "ply" line. 1127 | 1128 | Raises 1129 | ------ 1130 | PlyHeaderParseError 1131 | """ 1132 | self.format = None 1133 | self.elements = [] 1134 | self.comments = [] 1135 | self.obj_info = [] 1136 | self.lines = 1 1137 | self._allowed = ['format', 'comment', 'obj_info'] 1138 | for line in lines: 1139 | self.consume(line) 1140 | if self._allowed: 1141 | self._error("early end-of-file") 1142 | 1143 | def consume(self, raw_line): 1144 | """ 1145 | Parse and internalize one line of input. 1146 | """ 1147 | self.lines += 1 1148 | if not raw_line: 1149 | self._error("early end-of-file") 1150 | 1151 | line = raw_line.strip() 1152 | if line == '': 1153 | # We silently skip empty header lines. This isn't strictly 1154 | # allowed in the spec, but this logic slightly improves 1155 | # interoperability with other tools. 1156 | return self._allowed 1157 | try: 1158 | keyword = line.split(None, 1)[0] 1159 | except IndexError: 1160 | self._error() 1161 | 1162 | if keyword not in self._allowed: 1163 | self._error("expected one of {%s}" % 1164 | ", ".join(self._allowed)) 1165 | 1166 | # This dynamic method lookup pattern is somewhat questionable, 1167 | # but it's probably not worth replacing it with something more 1168 | # principled but also more complex. 1169 | getattr(self, 'parse_' + keyword)(line[len(keyword)+1:]) 1170 | return self._allowed 1171 | 1172 | def _error(self, message="parse error"): 1173 | raise PlyHeaderParseError(message, self.lines) 1174 | 1175 | # The parse_* methods below are used to parse all the different 1176 | # types of PLY header lines. (See `consume` above for the call site, 1177 | # which uses dynamic lookup.) Each method accepts a single argument, 1178 | # which is the remainder of the header line after the first word, 1179 | # and the method does two things: 1180 | # - internalize the semantic content of the string into the 1181 | # instance's attributes, and 1182 | # - set self._allowed to a list of the line types that can come 1183 | # next. 1184 | 1185 | def parse_format(self, data): 1186 | fields = data.strip().split() 1187 | if len(fields) != 2: 1188 | self._error("expected \"format {format} 1.0\"") 1189 | 1190 | self.format = fields[0] 1191 | if self.format not in _byte_order_map: 1192 | self._error("don't understand format %r" % self.format) 1193 | 1194 | if fields[1] != '1.0': 1195 | self._error("expected version '1.0'") 1196 | 1197 | self._allowed = ['element', 'comment', 'obj_info', 'end_header'] 1198 | 1199 | def parse_comment(self, data): 1200 | if not self.elements: 1201 | self.comments.append(data) 1202 | else: 1203 | self.elements[-1][3].append(data) 1204 | 1205 | def parse_obj_info(self, data): 1206 | self.obj_info.append(data) 1207 | 1208 | def parse_element(self, data): 1209 | fields = data.strip().split() 1210 | if len(fields) != 2: 1211 | self._error("expected \"element {name} {count}\"") 1212 | 1213 | name = fields[0] 1214 | try: 1215 | count = int(fields[1]) 1216 | except ValueError: 1217 | self._error("expected integer count") 1218 | 1219 | self.elements.append((name, [], count, [])) 1220 | self._allowed = ['element', 'comment', 'property', 'end_header'] 1221 | 1222 | def parse_property(self, data): 1223 | properties = self.elements[-1][1] 1224 | fields = data.strip().split() 1225 | if len(fields) < 2: 1226 | self._error("bad 'property' line") 1227 | 1228 | if fields[0] == 'list': 1229 | if len(fields) != 4: 1230 | self._error("expected \"property list " 1231 | "{len_type} {val_type} {name}\"") 1232 | 1233 | try: 1234 | properties.append( 1235 | PlyListProperty(fields[3], fields[1], fields[2]) 1236 | ) 1237 | except ValueError as e: 1238 | self._error(str(e)) 1239 | 1240 | else: 1241 | if len(fields) != 2: 1242 | self._error("expected \"property {type} {name}\"") 1243 | 1244 | try: 1245 | properties.append( 1246 | PlyProperty(fields[1], fields[0]) 1247 | ) 1248 | except ValueError as e: 1249 | self._error(str(e)) 1250 | 1251 | def parse_end_header(self, data): 1252 | if data: 1253 | self._error("unexpected data after 'end_header'") 1254 | self._allowed = [] 1255 | 1256 | 1257 | class _PlyHeaderLines(object): 1258 | """ 1259 | Generator over lines in the PLY header. 1260 | 1261 | LF, CR, and CRLF line endings are supported. 1262 | """ 1263 | 1264 | def __init__(self, stream): 1265 | """ 1266 | Parameters 1267 | ---------- 1268 | stream : text or binary stream. 1269 | 1270 | Raises 1271 | ------ 1272 | PlyHeaderParseError 1273 | """ 1274 | s = self._decode(stream.read(4)) 1275 | self.chars = [] 1276 | if s[:3] != 'ply': 1277 | raise PlyHeaderParseError("expected 'ply'", 1) 1278 | self.nl = s[3:] 1279 | if s[3:] == '\r': 1280 | c = self._decode(stream.read(1)) 1281 | if c == '\n': 1282 | self.nl += c 1283 | else: 1284 | self.chars.append(c) 1285 | elif s[3:] != '\n': 1286 | raise PlyHeaderParseError( 1287 | "unexpected characters after 'ply'", 1) 1288 | self.stream = stream 1289 | self.len_nl = len(self.nl) 1290 | self.done = False 1291 | self.lines = 1 1292 | 1293 | @staticmethod 1294 | def _decode(s): 1295 | """ 1296 | Convert input `str` or `bytes` instance to `str`, decoding 1297 | as ASCII if necessary. 1298 | """ 1299 | if isinstance(s, str): 1300 | return s 1301 | return s.decode('ascii') 1302 | 1303 | def __iter__(self): 1304 | """ 1305 | Yields 1306 | ------ 1307 | line : str 1308 | Decoded line with newline removed. 1309 | """ 1310 | while not self.done: 1311 | self.lines += 1 1312 | while ''.join(self.chars[-self.len_nl:]) != self.nl: 1313 | char = self._decode(self.stream.read(1)) 1314 | if not char: 1315 | raise PlyHeaderParseError("early end-of-file", 1316 | self.lines) 1317 | self.chars.append(char) 1318 | line = ''.join(self.chars[:-self.len_nl]) 1319 | self.chars = [] 1320 | if line == 'end_header': 1321 | self.done = True 1322 | yield line 1323 | 1324 | 1325 | def _open_stream(stream, read_or_write): 1326 | """ 1327 | Normalizing function: given a filename or open stream, 1328 | return an open stream. 1329 | 1330 | Parameters 1331 | ---------- 1332 | stream : str or open file-like object 1333 | read_or_write : str 1334 | `"read"` or `"write"`, the method to be used on the stream. 1335 | 1336 | Returns 1337 | ------- 1338 | must_close : bool 1339 | Whether `.close` needs to be called on the file object 1340 | by the caller (i.e., it wasn't already open). 1341 | file : file-like object 1342 | 1343 | Raises 1344 | ------ 1345 | TypeError 1346 | If `stream` is neither a string nor has the 1347 | `read_or_write`-indicated method. 1348 | """ 1349 | if hasattr(stream, read_or_write): 1350 | return (False, stream) 1351 | try: 1352 | return (True, open(stream, read_or_write[0] + 'b')) 1353 | except TypeError: 1354 | raise TypeError("expected open file or filename") 1355 | 1356 | 1357 | def _check_name(name): 1358 | """ 1359 | Check that a string can be safely be used as the name of an element 1360 | or property in a PLY file. 1361 | 1362 | Parameters 1363 | ---------- 1364 | name : str 1365 | 1366 | Raises 1367 | ------ 1368 | ValueError 1369 | If the check failed. 1370 | """ 1371 | for char in name: 1372 | if not 0 <= ord(char) < 128: 1373 | raise ValueError("non-ASCII character in name %r" % name) 1374 | if char.isspace(): 1375 | raise ValueError("space character(s) in name %r" % name) 1376 | 1377 | 1378 | def _check_comments(comments): 1379 | """ 1380 | Check that the given comments can be safely used in a PLY header. 1381 | 1382 | Parameters 1383 | ---------- 1384 | comments : list of str 1385 | 1386 | Raises 1387 | ------ 1388 | ValueError 1389 | If the check fails. 1390 | """ 1391 | for comment in comments: 1392 | for char in comment: 1393 | if not 0 <= ord(char) < 128: 1394 | raise ValueError("non-ASCII character in comment") 1395 | if char == '\n': 1396 | raise ValueError("embedded newline in comment") 1397 | 1398 | 1399 | def _read_array(stream, dtype, n): 1400 | """ 1401 | Read `n` elements of type `dtype` from an open stream. 1402 | 1403 | Parameters 1404 | ---------- 1405 | stream : readable open binary file 1406 | dtype : dtype description 1407 | n : int 1408 | 1409 | Returns 1410 | ------- 1411 | numpy.ndarray 1412 | 1413 | Raises 1414 | ------ 1415 | StopIteration 1416 | If `n` elements could not be read. 1417 | """ 1418 | try: 1419 | size = int(_np.dtype(dtype).itemsize) * int(n) 1420 | return _np.frombuffer(stream.read(size), dtype) 1421 | except Exception: 1422 | raise StopIteration 1423 | 1424 | 1425 | def _write_array(stream, array): 1426 | """ 1427 | Write `numpy` array to a binary file. 1428 | 1429 | Parameters 1430 | ---------- 1431 | stream : writeable open binary file 1432 | array : numpy.ndarray 1433 | """ 1434 | stream.write(array.tobytes()) 1435 | 1436 | 1437 | def _can_mmap(stream): 1438 | """ 1439 | Determine if a readable stream can be memory-mapped, using some good 1440 | heuristics. 1441 | 1442 | Parameters 1443 | ---------- 1444 | stream : open binary file 1445 | 1446 | Returns 1447 | ------- 1448 | bool 1449 | """ 1450 | try: 1451 | pos = stream.tell() 1452 | try: 1453 | _np.memmap(stream, 'u1', 'c') 1454 | stream.seek(pos) 1455 | return True 1456 | except Exception: 1457 | stream.seek(pos) 1458 | return False 1459 | except Exception: 1460 | return False 1461 | 1462 | 1463 | def _lookup_type(type_str): 1464 | if type_str not in _data_type_reverse: 1465 | try: 1466 | type_str = _data_types[type_str] 1467 | except KeyError: 1468 | raise ValueError("field type %r not in %r" % 1469 | (type_str, _types_list)) 1470 | 1471 | return _data_type_reverse[type_str] 1472 | 1473 | 1474 | # Many-many relation 1475 | _data_type_relation = [ 1476 | ('int8', 'i1'), 1477 | ('char', 'i1'), 1478 | ('uint8', 'u1'), 1479 | ('uchar', 'b1'), 1480 | ('uchar', 'u1'), 1481 | ('int16', 'i2'), 1482 | ('short', 'i2'), 1483 | ('uint16', 'u2'), 1484 | ('ushort', 'u2'), 1485 | ('int32', 'i4'), 1486 | ('int', 'i4'), 1487 | ('uint32', 'u4'), 1488 | ('uint', 'u4'), 1489 | ('float32', 'f4'), 1490 | ('float', 'f4'), 1491 | ('float64', 'f8'), 1492 | ('double', 'f8') 1493 | ] 1494 | 1495 | _data_types = dict(_data_type_relation) 1496 | _data_type_reverse = dict((b, a) for (a, b) in _data_type_relation) 1497 | 1498 | _types_list = [] 1499 | _types_set = set() 1500 | for (_a, _b) in _data_type_relation: 1501 | if _a not in _types_set: 1502 | _types_list.append(_a) 1503 | _types_set.add(_a) 1504 | if _b not in _types_set: 1505 | _types_list.append(_b) 1506 | _types_set.add(_b) 1507 | 1508 | 1509 | _byte_order_map = { 1510 | 'ascii': '=', 1511 | 'binary_little_endian': '<', 1512 | 'binary_big_endian': '>' 1513 | } 1514 | 1515 | _byte_order_reverse = { 1516 | '<': 'binary_little_endian', 1517 | '>': 'binary_big_endian' 1518 | } 1519 | 1520 | _native_byte_order = {'little': '<', 'big': '>'}[_byteorder] 1521 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default", "doc", "lint", "test"] 6 | strategy = [] 7 | lock_version = "4.5.0" 8 | content_hash = "sha256:c9289c8e3ae920ab4edee42fa2b402fd60090605ba569ad8e00517e2262a7e9d" 9 | 10 | [[metadata.targets]] 11 | requires_python = ">=3.9" 12 | 13 | [[package]] 14 | name = "alabaster" 15 | version = "0.7.16" 16 | requires_python = ">=3.9" 17 | summary = "A light, configurable Sphinx theme" 18 | files = [ 19 | {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, 20 | {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, 21 | ] 22 | 23 | [[package]] 24 | name = "babel" 25 | version = "2.15.0" 26 | requires_python = ">=3.8" 27 | summary = "Internationalization utilities" 28 | dependencies = [ 29 | "pytz>=2015.7; python_version < \"3.9\"", 30 | ] 31 | files = [ 32 | {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, 33 | {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, 34 | ] 35 | 36 | [[package]] 37 | name = "cachetools" 38 | version = "5.4.0" 39 | requires_python = ">=3.7" 40 | summary = "Extensible memoizing collections and decorators" 41 | files = [ 42 | {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, 43 | {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, 44 | ] 45 | 46 | [[package]] 47 | name = "certifi" 48 | version = "2024.7.4" 49 | requires_python = ">=3.6" 50 | summary = "Python package for providing Mozilla's CA Bundle." 51 | files = [ 52 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 53 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 54 | ] 55 | 56 | [[package]] 57 | name = "chardet" 58 | version = "5.2.0" 59 | requires_python = ">=3.7" 60 | summary = "Universal encoding detector for Python 3" 61 | files = [ 62 | {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, 63 | {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, 64 | ] 65 | 66 | [[package]] 67 | name = "charset-normalizer" 68 | version = "3.3.2" 69 | requires_python = ">=3.7.0" 70 | summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 71 | files = [ 72 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 73 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 74 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 75 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 76 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 77 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 78 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 79 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 80 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 81 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 82 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 83 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 84 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 85 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 86 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 87 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 88 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 89 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 90 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 91 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 92 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 93 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 94 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 95 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 96 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 97 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 98 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 99 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 100 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 101 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 102 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 103 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 104 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 105 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 106 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 107 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 108 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 109 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 110 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 111 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 112 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 113 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 114 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 115 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 116 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 117 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 118 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 119 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 120 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 121 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 122 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 123 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 124 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 125 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 126 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 127 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 128 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 129 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 130 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 131 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 132 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 133 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 134 | ] 135 | 136 | [[package]] 137 | name = "colorama" 138 | version = "0.4.6" 139 | requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 140 | summary = "Cross-platform colored terminal text." 141 | files = [ 142 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 143 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 144 | ] 145 | 146 | [[package]] 147 | name = "distlib" 148 | version = "0.3.8" 149 | summary = "Distribution utilities" 150 | files = [ 151 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 152 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 153 | ] 154 | 155 | [[package]] 156 | name = "docutils" 157 | version = "0.20.1" 158 | requires_python = ">=3.7" 159 | summary = "Docutils -- Python Documentation Utilities" 160 | files = [ 161 | {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, 162 | {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, 163 | ] 164 | 165 | [[package]] 166 | name = "exceptiongroup" 167 | version = "1.2.2" 168 | requires_python = ">=3.7" 169 | summary = "Backport of PEP 654 (exception groups)" 170 | files = [ 171 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 172 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 173 | ] 174 | 175 | [[package]] 176 | name = "filelock" 177 | version = "3.15.4" 178 | requires_python = ">=3.8" 179 | summary = "A platform independent file lock." 180 | files = [ 181 | {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, 182 | {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, 183 | ] 184 | 185 | [[package]] 186 | name = "flake8" 187 | version = "7.1.1" 188 | requires_python = ">=3.8.1" 189 | summary = "the modular source code checker: pep8 pyflakes and co" 190 | dependencies = [ 191 | "mccabe<0.8.0,>=0.7.0", 192 | "pycodestyle<2.13.0,>=2.12.0", 193 | "pyflakes<3.3.0,>=3.2.0", 194 | ] 195 | files = [ 196 | {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, 197 | {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, 198 | ] 199 | 200 | [[package]] 201 | name = "idna" 202 | version = "3.7" 203 | requires_python = ">=3.5" 204 | summary = "Internationalized Domain Names in Applications (IDNA)" 205 | files = [ 206 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 207 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 208 | ] 209 | 210 | [[package]] 211 | name = "imagesize" 212 | version = "1.4.1" 213 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 214 | summary = "Getting image size from png/jpeg/jpeg2000/gif file" 215 | files = [ 216 | {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, 217 | {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, 218 | ] 219 | 220 | [[package]] 221 | name = "importlib-metadata" 222 | version = "8.2.0" 223 | requires_python = ">=3.8" 224 | summary = "Read metadata from Python packages" 225 | dependencies = [ 226 | "typing-extensions>=3.6.4; python_version < \"3.8\"", 227 | "zipp>=0.5", 228 | ] 229 | files = [ 230 | {file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, 231 | {file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, 232 | ] 233 | 234 | [[package]] 235 | name = "iniconfig" 236 | version = "2.0.0" 237 | requires_python = ">=3.7" 238 | summary = "brain-dead simple config-ini parsing" 239 | files = [ 240 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 241 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 242 | ] 243 | 244 | [[package]] 245 | name = "jinja2" 246 | version = "3.1.4" 247 | requires_python = ">=3.7" 248 | summary = "A very fast and expressive template engine." 249 | dependencies = [ 250 | "MarkupSafe>=2.0", 251 | ] 252 | files = [ 253 | {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, 254 | {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, 255 | ] 256 | 257 | [[package]] 258 | name = "markdown-it-py" 259 | version = "3.0.0" 260 | requires_python = ">=3.8" 261 | summary = "Python port of markdown-it. Markdown parsing, done right!" 262 | dependencies = [ 263 | "mdurl~=0.1", 264 | ] 265 | files = [ 266 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 267 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 268 | ] 269 | 270 | [[package]] 271 | name = "markupsafe" 272 | version = "2.1.5" 273 | requires_python = ">=3.7" 274 | summary = "Safely add untrusted strings to HTML/XML markup." 275 | files = [ 276 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 277 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 278 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 279 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 280 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 281 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 282 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 283 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 284 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 285 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 286 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 287 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 288 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 289 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 290 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 291 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 292 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 293 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 294 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 295 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 296 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 297 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 298 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 299 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 300 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 301 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 302 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 303 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 304 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 305 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 306 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 307 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 308 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 309 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 310 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 311 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 312 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 313 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 314 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 315 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 316 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 317 | ] 318 | 319 | [[package]] 320 | name = "mccabe" 321 | version = "0.7.0" 322 | requires_python = ">=3.6" 323 | summary = "McCabe checker, plugin for flake8" 324 | files = [ 325 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 326 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 327 | ] 328 | 329 | [[package]] 330 | name = "mdit-py-plugins" 331 | version = "0.4.1" 332 | requires_python = ">=3.8" 333 | summary = "Collection of plugins for markdown-it-py" 334 | dependencies = [ 335 | "markdown-it-py<4.0.0,>=1.0.0", 336 | ] 337 | files = [ 338 | {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"}, 339 | {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"}, 340 | ] 341 | 342 | [[package]] 343 | name = "mdurl" 344 | version = "0.1.2" 345 | requires_python = ">=3.7" 346 | summary = "Markdown URL utilities" 347 | files = [ 348 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 349 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 350 | ] 351 | 352 | [[package]] 353 | name = "myst-parser" 354 | version = "3.0.1" 355 | requires_python = ">=3.8" 356 | summary = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," 357 | dependencies = [ 358 | "docutils<0.22,>=0.18", 359 | "jinja2", 360 | "markdown-it-py~=3.0", 361 | "mdit-py-plugins~=0.4", 362 | "pyyaml", 363 | "sphinx<8,>=6", 364 | ] 365 | files = [ 366 | {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, 367 | {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, 368 | ] 369 | 370 | [[package]] 371 | name = "numpy" 372 | version = "2.0.1" 373 | requires_python = ">=3.9" 374 | summary = "Fundamental package for array computing in Python" 375 | files = [ 376 | {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, 377 | {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, 378 | {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, 379 | {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, 380 | {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, 381 | {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, 382 | {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, 383 | {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, 384 | {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, 385 | {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, 386 | {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, 387 | {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, 388 | {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, 389 | {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, 390 | {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, 391 | {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, 392 | {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, 393 | {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, 394 | {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, 395 | {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, 396 | {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, 397 | {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, 398 | {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, 399 | {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, 400 | {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, 401 | {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, 402 | {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, 403 | {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, 404 | {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, 405 | {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, 406 | {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, 407 | {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, 408 | {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, 409 | {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, 410 | {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, 411 | {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, 412 | {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, 413 | {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, 414 | {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, 415 | {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, 416 | {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, 417 | {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, 418 | {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, 419 | {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, 420 | {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, 421 | ] 422 | 423 | [[package]] 424 | name = "numpydoc" 425 | version = "1.7.0" 426 | requires_python = ">=3.8" 427 | summary = "Sphinx extension to support docstrings in Numpy format" 428 | dependencies = [ 429 | "sphinx>=6", 430 | "tabulate>=0.8.10", 431 | "tomli>=1.1.0; python_version < \"3.11\"", 432 | ] 433 | files = [ 434 | {file = "numpydoc-1.7.0-py3-none-any.whl", hash = "sha256:5a56419d931310d79a06cfc2a126d1558700feeb9b4f3d8dcae1a8134be829c9"}, 435 | {file = "numpydoc-1.7.0.tar.gz", hash = "sha256:866e5ae5b6509dcf873fc6381120f5c31acf13b135636c1a81d68c166a95f921"}, 436 | ] 437 | 438 | [[package]] 439 | name = "packaging" 440 | version = "24.1" 441 | requires_python = ">=3.8" 442 | summary = "Core utilities for Python packages" 443 | files = [ 444 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 445 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 446 | ] 447 | 448 | [[package]] 449 | name = "platformdirs" 450 | version = "4.2.2" 451 | requires_python = ">=3.8" 452 | summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 453 | files = [ 454 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 455 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 456 | ] 457 | 458 | [[package]] 459 | name = "pluggy" 460 | version = "1.5.0" 461 | requires_python = ">=3.8" 462 | summary = "plugin and hook calling mechanisms for python" 463 | files = [ 464 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 465 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 466 | ] 467 | 468 | [[package]] 469 | name = "pycodestyle" 470 | version = "2.12.1" 471 | requires_python = ">=3.8" 472 | summary = "Python style guide checker" 473 | files = [ 474 | {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, 475 | {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, 476 | ] 477 | 478 | [[package]] 479 | name = "pyflakes" 480 | version = "3.2.0" 481 | requires_python = ">=3.8" 482 | summary = "passive checker of Python programs" 483 | files = [ 484 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 485 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 486 | ] 487 | 488 | [[package]] 489 | name = "pygments" 490 | version = "2.18.0" 491 | requires_python = ">=3.8" 492 | summary = "Pygments is a syntax highlighting package written in Python." 493 | files = [ 494 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 495 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 496 | ] 497 | 498 | [[package]] 499 | name = "pyproject-api" 500 | version = "1.7.1" 501 | requires_python = ">=3.8" 502 | summary = "API to interact with the python pyproject.toml based projects" 503 | dependencies = [ 504 | "packaging>=24.1", 505 | "tomli>=2.0.1; python_version < \"3.11\"", 506 | ] 507 | files = [ 508 | {file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"}, 509 | {file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"}, 510 | ] 511 | 512 | [[package]] 513 | name = "pytest" 514 | version = "8.3.2" 515 | requires_python = ">=3.8" 516 | summary = "pytest: simple powerful testing with Python" 517 | dependencies = [ 518 | "colorama; sys_platform == \"win32\"", 519 | "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", 520 | "iniconfig", 521 | "packaging", 522 | "pluggy<2,>=1.5", 523 | "tomli>=1; python_version < \"3.11\"", 524 | ] 525 | files = [ 526 | {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, 527 | {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, 528 | ] 529 | 530 | [[package]] 531 | name = "pyyaml" 532 | version = "6.0.1" 533 | requires_python = ">=3.6" 534 | summary = "YAML parser and emitter for Python" 535 | files = [ 536 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 537 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 538 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 539 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 540 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 541 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 542 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 543 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 544 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 545 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 546 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 547 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 548 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 549 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 550 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 551 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 552 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 553 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 554 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 555 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 556 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 557 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 558 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 559 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 560 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 561 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 562 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 563 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 564 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 565 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 566 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 567 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 568 | ] 569 | 570 | [[package]] 571 | name = "requests" 572 | version = "2.32.3" 573 | requires_python = ">=3.8" 574 | summary = "Python HTTP for Humans." 575 | dependencies = [ 576 | "certifi>=2017.4.17", 577 | "charset-normalizer<4,>=2", 578 | "idna<4,>=2.5", 579 | "urllib3<3,>=1.21.1", 580 | ] 581 | files = [ 582 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 583 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 584 | ] 585 | 586 | [[package]] 587 | name = "snowballstemmer" 588 | version = "2.2.0" 589 | summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 590 | files = [ 591 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 592 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 593 | ] 594 | 595 | [[package]] 596 | name = "sphinx" 597 | version = "7.4.7" 598 | requires_python = ">=3.9" 599 | summary = "Python documentation generator" 600 | dependencies = [ 601 | "Jinja2>=3.1", 602 | "Pygments>=2.17", 603 | "alabaster~=0.7.14", 604 | "babel>=2.13", 605 | "colorama>=0.4.6; sys_platform == \"win32\"", 606 | "docutils<0.22,>=0.20", 607 | "imagesize>=1.3", 608 | "importlib-metadata>=6.0; python_version < \"3.10\"", 609 | "packaging>=23.0", 610 | "requests>=2.30.0", 611 | "snowballstemmer>=2.2", 612 | "sphinxcontrib-applehelp", 613 | "sphinxcontrib-devhelp", 614 | "sphinxcontrib-htmlhelp>=2.0.0", 615 | "sphinxcontrib-jsmath", 616 | "sphinxcontrib-qthelp", 617 | "sphinxcontrib-serializinghtml>=1.1.9", 618 | "tomli>=2; python_version < \"3.11\"", 619 | ] 620 | files = [ 621 | {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, 622 | {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, 623 | ] 624 | 625 | [[package]] 626 | name = "sphinx-rtd-theme" 627 | version = "2.0.0" 628 | requires_python = ">=3.6" 629 | summary = "Read the Docs theme for Sphinx" 630 | dependencies = [ 631 | "docutils<0.21", 632 | "sphinx<8,>=5", 633 | "sphinxcontrib-jquery<5,>=4", 634 | ] 635 | files = [ 636 | {file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"}, 637 | {file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"}, 638 | ] 639 | 640 | [[package]] 641 | name = "sphinxcontrib-applehelp" 642 | version = "2.0.0" 643 | requires_python = ">=3.9" 644 | summary = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" 645 | files = [ 646 | {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, 647 | {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, 648 | ] 649 | 650 | [[package]] 651 | name = "sphinxcontrib-devhelp" 652 | version = "2.0.0" 653 | requires_python = ">=3.9" 654 | summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" 655 | files = [ 656 | {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, 657 | {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, 658 | ] 659 | 660 | [[package]] 661 | name = "sphinxcontrib-htmlhelp" 662 | version = "2.1.0" 663 | requires_python = ">=3.9" 664 | summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 665 | files = [ 666 | {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, 667 | {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, 668 | ] 669 | 670 | [[package]] 671 | name = "sphinxcontrib-jquery" 672 | version = "4.1" 673 | requires_python = ">=2.7" 674 | summary = "Extension to include jQuery on newer Sphinx releases" 675 | dependencies = [ 676 | "Sphinx>=1.8", 677 | ] 678 | files = [ 679 | {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, 680 | {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, 681 | ] 682 | 683 | [[package]] 684 | name = "sphinxcontrib-jsmath" 685 | version = "1.0.1" 686 | requires_python = ">=3.5" 687 | summary = "A sphinx extension which renders display math in HTML via JavaScript" 688 | files = [ 689 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 690 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 691 | ] 692 | 693 | [[package]] 694 | name = "sphinxcontrib-qthelp" 695 | version = "2.0.0" 696 | requires_python = ">=3.9" 697 | summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" 698 | files = [ 699 | {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, 700 | {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, 701 | ] 702 | 703 | [[package]] 704 | name = "sphinxcontrib-serializinghtml" 705 | version = "2.0.0" 706 | requires_python = ">=3.9" 707 | summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" 708 | files = [ 709 | {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, 710 | {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, 711 | ] 712 | 713 | [[package]] 714 | name = "tabulate" 715 | version = "0.9.0" 716 | requires_python = ">=3.7" 717 | summary = "Pretty-print tabular data" 718 | files = [ 719 | {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, 720 | {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, 721 | ] 722 | 723 | [[package]] 724 | name = "tomli" 725 | version = "2.0.1" 726 | requires_python = ">=3.7" 727 | summary = "A lil' TOML parser" 728 | files = [ 729 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 730 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 731 | ] 732 | 733 | [[package]] 734 | name = "tox" 735 | version = "4.16.0" 736 | requires_python = ">=3.8" 737 | summary = "tox is a generic virtualenv management and test command line tool" 738 | dependencies = [ 739 | "cachetools>=5.3.3", 740 | "chardet>=5.2", 741 | "colorama>=0.4.6", 742 | "filelock>=3.15.4", 743 | "packaging>=24.1", 744 | "platformdirs>=4.2.2", 745 | "pluggy>=1.5", 746 | "pyproject-api>=1.7.1", 747 | "tomli>=2.0.1; python_version < \"3.11\"", 748 | "virtualenv>=20.26.3", 749 | ] 750 | files = [ 751 | {file = "tox-4.16.0-py3-none-any.whl", hash = "sha256:61e101061b977b46cf00093d4319438055290ad0009f84497a07bf2d2d7a06d0"}, 752 | {file = "tox-4.16.0.tar.gz", hash = "sha256:43499656f9949edb681c0f907f86fbfee98677af9919d8b11ae5ad77cb800748"}, 753 | ] 754 | 755 | [[package]] 756 | name = "tox-gh-actions" 757 | version = "3.2.0" 758 | requires_python = ">=3.7" 759 | summary = "Seamless integration of tox into GitHub Actions" 760 | dependencies = [ 761 | "tox<5,>=4", 762 | ] 763 | files = [ 764 | {file = "tox-gh-actions-3.2.0.tar.gz", hash = "sha256:ac6fa3b8da51bc90dd77985fd55f09e746c6558c55910c0a93d643045a2b0ccc"}, 765 | {file = "tox_gh_actions-3.2.0-py2.py3-none-any.whl", hash = "sha256:821b66a4751a788fa3e9617bd796d696507b08c6e1d929ee4faefba06b73b694"}, 766 | ] 767 | 768 | [[package]] 769 | name = "urllib3" 770 | version = "2.2.2" 771 | requires_python = ">=3.8" 772 | summary = "HTTP library with thread-safe connection pooling, file post, and more." 773 | files = [ 774 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 775 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 776 | ] 777 | 778 | [[package]] 779 | name = "virtualenv" 780 | version = "20.26.3" 781 | requires_python = ">=3.7" 782 | summary = "Virtual Python Environment builder" 783 | dependencies = [ 784 | "distlib<1,>=0.3.7", 785 | "filelock<4,>=3.12.2", 786 | "importlib-metadata>=6.6; python_version < \"3.8\"", 787 | "platformdirs<5,>=3.9.1", 788 | ] 789 | files = [ 790 | {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, 791 | {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, 792 | ] 793 | 794 | [[package]] 795 | name = "zipp" 796 | version = "3.19.2" 797 | requires_python = ">=3.8" 798 | summary = "Backport of pathlib-compatible object wrapper for zip files" 799 | files = [ 800 | {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, 801 | {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, 802 | ] 803 | --------------------------------------------------------------------------------