├── 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 | 
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 |
--------------------------------------------------------------------------------