diff --git a/.github/workflows/CI-test.yml b/.github/workflows/CI-test.yml new file mode 100644 index 0000000..bebd035 --- /dev/null +++ b/.github/workflows/CI-test.yml @@ -0,0 +1,56 @@ +name: CI-test + +on: + push: + branches: + - main + paths-ignore: + - README.md + - LICENSE + pull_request: + paths-ignore: + - README.md + - LICENSE + workflow_dispatch: + +env: + GITHUB_ACTIONS: true + +jobs: + test: + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + os-version: ["ubuntu-20.04", "windows-latest", "macos-13"] + + runs-on: ${{ matrix.os-version }} + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - uses: pdm-project/setup-pdm@v4 + name: Setup PDM + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + version: 2.19.2 + prerelease: false + enable-pep582: false + allow-python-prereleases: false + update-python: true + + - name: Install mediainfo lib for Linux + if: matrix.os-version == 'ubuntu-20.04' + run: | + sudo apt install libmediainfo-dev -y + sudo apt install libgl1-mesa-glx -y + + - name: Test + run: | + pdm install + pdm test diff --git a/.github/workflows/Release-pypi.yml b/.github/workflows/Release-pypi.yml new file mode 100644 index 0000000..d11fd08 --- /dev/null +++ b/.github/workflows/Release-pypi.yml @@ -0,0 +1,56 @@ +name: Release-pypi + +on: + workflow_dispatch: + +jobs: + Pypi: + strategy: + matrix: + python-version: ["3.8"] + os-version: ["ubuntu-20.04", "windows-latest", "macos-13"] + + runs-on: ${{ matrix.os-version }} + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - uses: pdm-project/setup-pdm@v3 + name: Setup PDM + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + version: 2.11.1 + prerelease: false + enable-pep582: false + allow-python-prereleases: false + update-python: true + + - name: Install package + run: | + pdm install + + - name: Build package Linux + if: matrix.os-version == 'ubuntu-20.04' + run: | + pdm build --no-sdist --config-setting="--plat-name=manylinux1" + + - name: Build package Windows amd64 + if: matrix.os-version == 'windows-latest' + run: | + pdm build --no-sdist --config-setting="--plat-name=win_amd64" + + - name: Build package MacOS universal2 + if: matrix.os-version == 'macos-13' + run: | + pdm build --no-sdist --config-setting="--plat-name=macosx_11_0_universal2" + + - name: Publish a Python distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20d1073 --- /dev/null +++ b/.gitignore @@ -0,0 +1,167 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +*.DS_Store + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +/.ruff_cache/ + +*.so +*.dll +*.dylib diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1b3db4b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,44 @@ +repos: + - repo: https://github.com/pdm-project/pdm + rev: 2.11.1 # a PDM release exposing the hook + hooks: + - id: pdm-sync + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-json + - id: check-yaml + - id: check-toml + + # autofix json, yaml, markdown... + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + + # autofix toml + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.12.0 + hooks: + - id: pretty-format-toml + args: [--autofix] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.6 + hooks: + - id: ruff-format + - id: ruff + args: [--fix, --exit-non-zero-on-fix] +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: v1.7.1 +# hooks: +# - id: mypy +# args: [pymediainfo, tests] +# pass_filenames: false +# additional_dependencies: +# - types-requests +# - types-certifi +# - pytest diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4b11f67 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +The MIT License + +Copyright (c) 2010-2014, Patrick Altman +Copyright (c) 2016, Louis Sautier +Copyright (c) 2024, TensoRaws + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +http://www.opensource.org/licenses/mit-license.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/demo.py b/demo.py new file mode 100755 index 0000000..b432d9a --- /dev/null +++ b/demo.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +""" +a demo that shows how to call pymediainfo +""" + +import sys +from pprint import pprint + +from pymediainfo import MediaInfo + + +def print_frame(text): + print("+-{}-+".format("-" * len(text))) + print("| {} |".format(text)) + print("+-{}-+".format("-" * len(text))) + + +def process(fname): + media_info = MediaInfo.parse(fname) + for track in media_info.tracks: + print_frame(track.track_type) + pprint(track.to_data()) + print() + for track in media_info.tracks: + if track.track_type == "General" and track.duration: + print("Duration: {} sec.".format(track.duration / 1000.0)) + + +if __name__ == "__main__": + if len(sys.argv) == 1: + print("Usage: {} ".format(sys.argv[0])) + sys.exit(0) + process(sys.argv[1]) diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..bcc13f6 --- /dev/null +++ b/pdm.lock @@ -0,0 +1,680 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "lint", "test"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:5e49ced82cad3f1003a2aa75cf4bf615fa5714416382369b53c1e9fb2afbdc1f" + +[[metadata.targets]] +requires_python = ">=3.8" + +[[package]] +name = "certifi" +version = "2024.8.30" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["test"] +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cfgv" +version = "3.3.1" +requires_python = ">=3.6.1" +summary = "Validate configuration and produce human readable error messages." +groups = ["lint"] +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["test"] +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["test"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +requires_python = ">=3.7" +summary = "Code coverage measurement for Python" +groups = ["test"] +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[[package]] +name = "coverage" +version = "7.2.7" +extras = ["toml"] +requires_python = ">=3.7" +summary = "Code coverage measurement for Python" +groups = ["test"] +dependencies = [ + "coverage==7.2.7", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +summary = "Distribution utilities" +groups = ["lint"] +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["test"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[[package]] +name = "filelock" +version = "3.12.2" +requires_python = ">=3.7" +summary = "A platform independent file lock." +groups = ["lint"] +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[[package]] +name = "identify" +version = "2.5.24" +requires_python = ">=3.7" +summary = "File identification library for Python" +groups = ["lint"] +files = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] + +[[package]] +name = "idna" +version = "3.10" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["test"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mypy" +version = "1.4.1" +requires_python = ">=3.7" +summary = "Optional static typing for Python" +groups = ["lint"] +dependencies = [ + "mypy-extensions>=1.0.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "typed-ast<2,>=1.4.0; python_version < \"3.8\"", + "typing-extensions>=4.1.0", +] +files = [ + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +requires_python = ">=3.5" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["lint"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +summary = "Node.js virtual environment builder" +groups = ["lint"] +dependencies = [ + "setuptools", +] +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[[package]] +name = "packaging" +version = "23.2" +requires_python = ">=3.7" +summary = "Core utilities for Python packages" +groups = ["test"] +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "platformdirs" +version = "4.0.0" +requires_python = ">=3.7" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +groups = ["lint"] +dependencies = [ + "typing-extensions>=4.7.1; python_version < \"3.8\"", +] +files = [ + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +requires_python = ">=3.7" +summary = "plugin and hook calling mechanisms for python" +groups = ["test"] +dependencies = [ + "importlib-metadata>=0.12; python_version < \"3.8\"", +] +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[[package]] +name = "pre-commit" +version = "2.21.0" +requires_python = ">=3.7" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +groups = ["lint"] +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "importlib-metadata; python_version < \"3.8\"", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] + +[[package]] +name = "pytest" +version = "7.4.3" +requires_python = ">=3.7" +summary = "pytest: simple powerful testing with Python" +groups = ["test"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "importlib-metadata>=0.12; python_version < \"3.8\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=0.12", + "tomli>=1.0.0; python_version < \"3.11\"", +] +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +requires_python = ">=3.7" +summary = "Pytest plugin for measuring coverage." +groups = ["test"] +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +requires_python = ">=3.6" +summary = "YAML parser and emitter for Python" +groups = ["lint"] +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +requires_python = ">=3.8" +summary = "Python HTTP for Humans." +groups = ["test"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[[package]] +name = "ruff" +version = "0.1.9" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +groups = ["lint"] +files = [ + {file = "ruff-0.1.9-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e6a212f436122ac73df851f0cf006e0c6612fe6f9c864ed17ebefce0eff6a5fd"}, + {file = "ruff-0.1.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:28d920e319783d5303333630dae46ecc80b7ba294aeffedf946a02ac0b7cc3db"}, + {file = "ruff-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:104aa9b5e12cb755d9dce698ab1b97726b83012487af415a4512fedd38b1459e"}, + {file = "ruff-0.1.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e63bf5a4a91971082a4768a0aba9383c12392d0d6f1e2be2248c1f9054a20da"}, + {file = "ruff-0.1.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d0738917c203246f3e275b37006faa3aa96c828b284ebfe3e99a8cb413c8c4b"}, + {file = "ruff-0.1.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69dac82d63a50df2ab0906d97a01549f814b16bc806deeac4f064ff95c47ddf5"}, + {file = "ruff-0.1.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2aec598fb65084e41a9c5d4b95726173768a62055aafb07b4eff976bac72a592"}, + {file = "ruff-0.1.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:744dfe4b35470fa3820d5fe45758aace6269c578f7ddc43d447868cfe5078bcb"}, + {file = "ruff-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:479ca4250cab30f9218b2e563adc362bd6ae6343df7c7b5a7865300a5156d5a6"}, + {file = "ruff-0.1.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:aa8344310f1ae79af9ccd6e4b32749e93cddc078f9b5ccd0e45bd76a6d2e8bb6"}, + {file = "ruff-0.1.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:837c739729394df98f342319f5136f33c65286b28b6b70a87c28f59354ec939b"}, + {file = "ruff-0.1.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e6837202c2859b9f22e43cb01992373c2dbfeae5c0c91ad691a4a2e725392464"}, + {file = "ruff-0.1.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:331aae2cd4a0554667ac683243b151c74bd60e78fb08c3c2a4ac05ee1e606a39"}, + {file = "ruff-0.1.9-py3-none-win32.whl", hash = "sha256:8151425a60878e66f23ad47da39265fc2fad42aed06fb0a01130e967a7a064f4"}, + {file = "ruff-0.1.9-py3-none-win_amd64.whl", hash = "sha256:c497d769164df522fdaf54c6eba93f397342fe4ca2123a2e014a5b8fc7df81c7"}, + {file = "ruff-0.1.9-py3-none-win_arm64.whl", hash = "sha256:0e17f53bcbb4fff8292dfd84cf72d767b5e146f009cccd40c2fad27641f8a7a9"}, + {file = "ruff-0.1.9.tar.gz", hash = "sha256:b041dee2734719ddbb4518f762c982f2e912e7f28b8ee4fe1dee0b15d1b6e800"}, +] + +[[package]] +name = "setuptools" +version = "68.0.0" +requires_python = ">=3.7" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +groups = ["lint"] +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +groups = ["lint", "test"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tqdm" +version = "4.66.5" +requires_python = ">=3.7" +summary = "Fast, Extensible Progress Meter" +groups = ["test"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, + {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +requires_python = ">=3.7" +summary = "Backported and Experimental Type Hints for Python 3.7+" +groups = ["lint"] +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["test"] +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[[package]] +name = "virtualenv" +version = "20.25.0" +requires_python = ">=3.7" +summary = "Virtual Python Environment builder" +groups = ["lint"] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<5,>=3.9.1", +] +files = [ + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, +] diff --git a/pymediainfo/__init__.py b/pymediainfo/__init__.py new file mode 100644 index 0000000..af319cf --- /dev/null +++ b/pymediainfo/__init__.py @@ -0,0 +1,526 @@ +# vim: set fileencoding=utf-8 : +""" +This module is a wrapper around the MediaInfo library. +""" +import ctypes +import json +import os +import pathlib +import re +import sys +import warnings +import xml.etree.ElementTree as ET +from typing import Any, Dict, List, Optional, Tuple, Union + +try: + from importlib import metadata +except ImportError: + import importlib_metadata as metadata # type: ignore + +try: + __version__ = metadata.version("pymediainfo") +except metadata.PackageNotFoundError: + __version__ = "" + + +class Track: + """ + An object associated with a media file track. + + Each :class:`Track` attribute corresponds to attributes parsed from MediaInfo's output. + All attributes are lower case. Attributes that are present several times such as `Duration` + yield a second attribute starting with `other_` which is a list of all alternative + attribute values. + + When a non-existing attribute is accessed, `None` is returned. + + Example: + + >>> t = mi.tracks[0] + >>> t + + >>> t.duration + 3000 + >>> t.other_duration + ['3 s 0 ms', '3 s 0 ms', '3 s 0 ms', + '00:00:03.000', '00:00:03.000'] + >>> type(t.non_existing) + NoneType + + All available attributes can be obtained by calling :func:`to_data`. + """ + + def __eq__(self, other): # type: ignore + return self.__dict__ == other.__dict__ + + def __getattribute__(self, name): # type: ignore + try: + return object.__getattribute__(self, name) + except AttributeError: + pass + return None + + def __getstate__(self): # type: ignore + return self.__dict__ + + def __setstate__(self, state): # type: ignore + self.__dict__ = state + + def __init__(self, xml_dom_fragment: ET.Element): + self.track_type = xml_dom_fragment.attrib["type"] + repeated_attributes = [] + for elem in xml_dom_fragment: + node_name = elem.tag.lower().strip().strip("_") + if node_name == "id": + node_name = "track_id" + node_value = elem.text + if getattr(self, node_name) is None: + setattr(self, node_name, node_value) + else: + other_node_name = f"other_{node_name}" + repeated_attributes.append((node_name, other_node_name)) + if getattr(self, other_node_name) is None: + setattr(self, other_node_name, [node_value]) + else: + getattr(self, other_node_name).append(node_value) + + for primary_key, other_key in repeated_attributes: + try: + # Attempt to convert the main value to int + # Usually, if an attribute is repeated, one of its value + # is an int and others are human-readable formats + setattr(self, primary_key, int(getattr(self, primary_key))) + except ValueError: + # If it fails, try to find a secondary value + # that is an int and swap it with the main value + for other_value in getattr(self, other_key): + try: + current = getattr(self, primary_key) + # Set the main value to an int + setattr(self, primary_key, int(other_value)) + # Append its previous value to other values + getattr(self, other_key).append(current) + break + except ValueError: + pass + + def __repr__(self): # type: ignore + return "".format(self.track_id, self.track_type) + + def to_data(self) -> Dict[str, Any]: + """ + Returns a dict representation of the track attributes. + + Example: + + >>> sorted(track.to_data().keys())[:3] + ['codec', 'codec_extensions_usually_used', 'codec_url'] + >>> t.to_data()["file_size"] + 5988 + + + :rtype: dict + """ + return self.__dict__ + + +class MediaInfo: + """ + An object containing information about a media file. + + + :class:`MediaInfo` objects can be created by directly calling code from + libmediainfo (in this case, the library must be present on the system): + + >>> pymediainfo.MediaInfo.parse("/path/to/file.mp4") + + Alternatively, objects may be created from MediaInfo's XML output. + Such output can be obtained using the ``XML`` output format on versions older than v17.10 + and the ``OLDXML`` format on newer versions. + + Using such an XML file, we can create a :class:`MediaInfo` object: + + >>> with open("output.xml") as f: + ... mi = pymediainfo.MediaInfo(f.read()) + + :param str xml: XML output obtained from MediaInfo. + :param str encoding_errors: option to pass to :func:`str.encode`'s `errors` + parameter before parsing `xml`. + :raises xml.etree.ElementTree.ParseError: if passed invalid XML. + :var tracks: A list of :py:class:`Track` objects which the media file contains. + For instance: + + >>> mi = pymediainfo.MediaInfo.parse("/path/to/file.mp4") + >>> for t in mi.tracks: + ... print(t) + + + """ + + def __eq__(self, other): # type: ignore + return self.tracks == other.tracks + + def __init__(self, xml: str, encoding_errors: str = "strict"): + xml_dom = ET.fromstring(xml.encode("utf-8", encoding_errors)) + self.tracks = [] + # This is the case for libmediainfo < 18.03 + # https://github.com/sbraz/pymediainfo/issues/57 + # https://github.com/MediaArea/MediaInfoLib/commit/575a9a32e6960ea34adb3bc982c64edfa06e95eb + if xml_dom.tag == "File": + xpath = "track" + else: + xpath = "File/track" + for xml_track in xml_dom.iterfind(xpath): + self.tracks.append(Track(xml_track)) + + def _tracks(self, track_type: str) -> List[Track]: + return [track for track in self.tracks if track.track_type == track_type] + + @property + def general_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``General``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("General") + + @property + def video_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Video``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Video") + + @property + def audio_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Audio``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Audio") + + @property + def text_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Text``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Text") + + @property + def other_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Other``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Other") + + @property + def image_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Image``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Image") + + @property + def menu_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Menu``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Menu") + + @staticmethod + def _normalize_filename(filename: Any) -> Any: + if hasattr(os, "PathLike") and isinstance(filename, os.PathLike): + return os.fspath(filename) + if pathlib is not None and isinstance(filename, pathlib.PurePath): + return str(filename) + return filename + + @classmethod + def _define_library_prototypes(cls, lib: Any) -> Any: + lib.MediaInfo_Inform.restype = ctypes.c_wchar_p + lib.MediaInfo_New.argtypes = [] + lib.MediaInfo_New.restype = ctypes.c_void_p + lib.MediaInfo_Option.argtypes = [ + ctypes.c_void_p, + ctypes.c_wchar_p, + ctypes.c_wchar_p, + ] + lib.MediaInfo_Option.restype = ctypes.c_wchar_p + lib.MediaInfo_Inform.argtypes = [ctypes.c_void_p, ctypes.c_size_t] + lib.MediaInfo_Inform.restype = ctypes.c_wchar_p + lib.MediaInfo_Open.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p] + lib.MediaInfo_Open.restype = ctypes.c_size_t + lib.MediaInfo_Open_Buffer_Init.argtypes = [ + ctypes.c_void_p, + ctypes.c_uint64, + ctypes.c_uint64, + ] + lib.MediaInfo_Open_Buffer_Init.restype = ctypes.c_size_t + lib.MediaInfo_Open_Buffer_Continue.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_size_t, + ] + lib.MediaInfo_Open_Buffer_Continue.restype = ctypes.c_size_t + lib.MediaInfo_Open_Buffer_Continue_GoTo_Get.argtypes = [ctypes.c_void_p] + lib.MediaInfo_Open_Buffer_Continue_GoTo_Get.restype = ctypes.c_uint64 + lib.MediaInfo_Open_Buffer_Finalize.argtypes = [ctypes.c_void_p] + lib.MediaInfo_Open_Buffer_Finalize.restype = ctypes.c_size_t + lib.MediaInfo_Delete.argtypes = [ctypes.c_void_p] + lib.MediaInfo_Delete.restype = None + lib.MediaInfo_Close.argtypes = [ctypes.c_void_p] + lib.MediaInfo_Close.restype = None + + @staticmethod + def _get_library_paths(os_is_nt: bool) -> Tuple[str, ...]: + library_paths: Tuple[str, ...] + if os_is_nt: + library_paths = ("MediaInfo.dll",) + elif sys.platform == "darwin": + library_paths = ("libmediainfo.0.dylib", "libmediainfo.dylib") + else: + library_paths = ("libmediainfo.so.0",) + script_dir = os.path.dirname(__file__) + # Look for the library file in the script folder + for library in library_paths: + absolute_library_path = os.path.join(script_dir, library) + if os.path.isfile(absolute_library_path): + # If we find it, don't try any other filename + library_paths = (absolute_library_path,) + break + return library_paths + + @classmethod + def _get_library( + cls, + library_file: Optional[str] = None, + ) -> Tuple[Any, Any, str, Tuple[int, ...]]: + os_is_nt = os.name in ("nt", "dos", "os2", "ce") + if os_is_nt: + lib_type = ctypes.WinDLL # type: ignore + else: + lib_type = ctypes.CDLL + if library_file is None: + library_paths = cls._get_library_paths(os_is_nt) + else: + library_paths = (library_file,) + exceptions = [] + for library_path in library_paths: + try: + lib = lib_type(library_path) + cls._define_library_prototypes(lib) + # Without a handle, there might be problems when using concurrent threads + # https://github.com/sbraz/pymediainfo/issues/76#issuecomment-574759621 + handle = lib.MediaInfo_New() + version = lib.MediaInfo_Option(handle, "Info_Version", "") + match = re.search(r"^MediaInfoLib - v(\S+)", version) + if match: + lib_version_str = match.group(1) + lib_version = tuple(int(_) for _ in lib_version_str.split(".")) + else: + raise RuntimeError("Could not determine library version") + return (lib, handle, lib_version_str, lib_version) + except OSError as exc: + exceptions.append(str(exc)) + raise OSError("Failed to load library from {} - {}".format(", ".join(library_paths), ", ".join(exceptions))) + + @classmethod + def can_parse(cls, library_file: Optional[str] = None) -> bool: + """ + Checks whether media files can be analyzed using libmediainfo. + + :param str library_file: path to the libmediainfo library, this should only be used if + the library cannot be auto-detected. + :rtype: bool + """ + try: + lib, handle = cls._get_library(library_file)[:2] + lib.MediaInfo_Close(handle) + lib.MediaInfo_Delete(handle) + return True + except Exception: # pylint: disable=broad-except + return False + + @classmethod + def parse( + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches, too-many-locals, too-many-arguments + cls, + filename: Any, + library_file: Optional[str] = None, + cover_data: bool = False, + encoding_errors: str = "strict", + parse_speed: float = 0.5, + full: bool = True, + legacy_stream_display: bool = False, + mediainfo_options: Optional[Dict[str, str]] = None, + output: Optional[str] = None, + buffer_size: Optional[int] = 64 * 1024, + ) -> Union[str, "MediaInfo"]: + """ + Analyze a media file using libmediainfo. + + .. note:: + Because of the way the underlying library works, this method should not + be called simultaneously from multiple threads *with different arguments*. + Doing so will cause inconsistencies or failures by changing + library options that are shared across threads. + + :param filename: path to the media file or file-like object which will be analyzed. + A URL can also be used if libmediainfo was compiled + with CURL support. + :param str library_file: path to the libmediainfo library, this should only be used if + the library cannot be auto-detected. + :param bool cover_data: whether to retrieve cover data as base64. + :param str encoding_errors: option to pass to :func:`str.encode`'s `errors` + parameter before parsing MediaInfo's XML output. + :param float parse_speed: passed to the library as `ParseSpeed`, + this option takes values between 0 and 1. + A higher value will yield more precise results in some cases + but will also increase parsing time. + :param bool full: display additional tags, including computer-readable values + for sizes and durations, corresponds to the CLI's ``--Full``/``-f`` parameter. + :param bool legacy_stream_display: display additional information about streams. + :param dict mediainfo_options: additional options that will be passed to the + `MediaInfo_Option` function, for example: ``{"Language": "raw"}``. + Do not use this parameter when running the method simultaneously from multiple threads, + it will trigger a reset of all options which will cause inconsistencies or failures. + :param str output: custom output format for MediaInfo, corresponds to the CLI's + ``--Output`` parameter. Setting this causes the method to + return a `str` instead of a :class:`MediaInfo` object. + + Useful values include: + * the empty `str` ``""`` (corresponds to the default + text output, obtained when running ``mediainfo`` with no + additional parameters) + + * ``"XML"`` + + * ``"JSON"`` + + * ``%``-delimited templates (see ``mediainfo --Info-Parameters``) + :param int buffer_size: size of the buffer used to read the file, in bytes. This is only + used when `filename` is a file-like object. + :type filename: str or pathlib.Path or os.PathLike or file-like object. + :rtype: str if `output` is set. + :rtype: :class:`MediaInfo` otherwise. + :raises FileNotFoundError: if passed a non-existent file. + :raises ValueError: if passed a file-like object opened in text mode. + :raises OSError: if the library file could not be loaded. + :raises RuntimeError: if parsing fails, this should not + happen unless libmediainfo itself fails. + + Examples: + >>> pymediainfo.MediaInfo.parse("tests/data/sample.mkv") + + + >>> import json + >>> mi = pymediainfo.MediaInfo.parse("tests/data/sample.mkv", + ... output="JSON") + >>> json.loads(mi)["media"]["track"][0] + {'@type': 'General', 'TextCount': '1', 'FileExtension': 'mkv', + 'FileSize': '5904', … } + + + """ + lib, handle, lib_version_str, lib_version = cls._get_library(library_file) + # The XML option was renamed starting with version 17.10 + if lib_version >= (17, 10): + xml_option = "OLDXML" + else: + xml_option = "XML" + # Cover_Data is not extracted by default since version 18.03 + # See https://github.com/MediaArea/MediaInfoLib/commit/d8fd88a1 + if lib_version >= (18, 3): + lib.MediaInfo_Option(handle, "Cover_Data", "base64" if cover_data else "") + lib.MediaInfo_Option(handle, "CharSet", "UTF-8") + lib.MediaInfo_Option(handle, "Inform", xml_option if output is None else output) + lib.MediaInfo_Option(handle, "Complete", "1" if full else "") + lib.MediaInfo_Option(handle, "ParseSpeed", str(parse_speed)) + lib.MediaInfo_Option(handle, "LegacyStreamDisplay", "1" if legacy_stream_display else "") + if mediainfo_options is not None: + if lib_version < (19, 9): + warnings.warn( + "This version of MediaInfo (v{}) does not support resetting all " + "options to their default values, passing it custom options is not recommended " + "and may result in unpredictable behavior, see " + "https://github.com/MediaArea/MediaInfoLib/issues/1128".format(lib_version_str), + RuntimeWarning, + ) + for option_name, option_value in mediainfo_options.items(): + lib.MediaInfo_Option(handle, option_name, option_value) + try: + filename.seek(0, 2) + file_size = filename.tell() + filename.seek(0) + except AttributeError: # filename is not a file-like object + file_size = None + + if file_size is not None: # We have a file-like object, use the buffer protocol: + # Some file-like objects do not have a mode + if "b" not in getattr(filename, "mode", "b"): + raise ValueError("File should be opened in binary mode") + lib.MediaInfo_Open_Buffer_Init(handle, file_size, 0) + while True: + buffer = filename.read(buffer_size) + if buffer: + # https://github.com/MediaArea/MediaInfoLib/blob/v20.09/Source/MediaInfo/File__Analyze.h#L1429 + # 4th bit = finished + if lib.MediaInfo_Open_Buffer_Continue(handle, buffer, len(buffer)) & 0x08: + break + # Ask MediaInfo if we need to seek + seek = lib.MediaInfo_Open_Buffer_Continue_GoTo_Get(handle) + # https://github.com/MediaArea/MediaInfoLib/blob/v20.09/Source/MediaInfoDLL/MediaInfoJNI.cpp#L127 + if seek != ctypes.c_uint64(-1).value: + filename.seek(seek) + # Inform MediaInfo we have sought + lib.MediaInfo_Open_Buffer_Init(handle, file_size, filename.tell()) + else: + break + lib.MediaInfo_Open_Buffer_Finalize(handle) + else: # We have a filename, simply pass it: + filename = cls._normalize_filename(filename) + # If an error occured + if lib.MediaInfo_Open(handle, filename) == 0: + lib.MediaInfo_Close(handle) + lib.MediaInfo_Delete(handle) + # If filename doesn't look like a URL and doesn't exist + if "://" not in filename and not os.path.exists(filename): + raise FileNotFoundError(filename) + # We ran into another kind of error + raise RuntimeError("An error occured while opening {}" " with libmediainfo".format(filename)) + info: str = lib.MediaInfo_Inform(handle, 0) + # Reset all options to their defaults so that they aren't + # retained when the parse method is called several times + # https://github.com/MediaArea/MediaInfoLib/issues/1128 + # Do not call it when it is not required because it breaks threads + # https://github.com/sbraz/pymediainfo/issues/76#issuecomment-575245093 + if mediainfo_options is not None and lib_version >= (19, 9): + lib.MediaInfo_Option(handle, "Reset", "") + # Delete the handle + lib.MediaInfo_Close(handle) + lib.MediaInfo_Delete(handle) + if output is None: + return cls(info, encoding_errors) + return info + + def to_data(self) -> Dict[str, Any]: + """ + Returns a dict representation of the object's :py:class:`Tracks `. + + :rtype: dict + """ + return {"tracks": [_.to_data() for _ in self.tracks]} + + def to_json(self) -> str: + """ + Returns a JSON representation of the object's :py:class:`Tracks `. + + :rtype: str + """ + return json.dumps(self.to_data()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5f291f4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,105 @@ +[build-system] +build-backend = "pdm.backend" +requires = ["pdm-backend"] + +[project] +authors = [ + {name = "Tohrusky", email = "65994850+Tohrusky@users.noreply.github.com"} +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "License :: OSI Approved :: MIT License" +] +dependencies = [] +license = {text = "MIT"} +name = "py-mediainfo" +readme = "README.md" +requires-python = ">=3.8" +version = "0.0.1" + +[project.urls] +Homepage = "https://github.com/TensoRaws/py-mediainfo" +Repository = "https://github.com/TensoRaws/py-mediainfo" + +[tool.coverage.run] +omit = [] + +[tool.mypy] +disable_error_code = "attr-defined" +disallow_any_generics = false +ignore_missing_imports = true +strict = true +warn_return_any = false + +[tool.pdm.dev-dependencies] +lint = [ + "pre-commit", + "ruff", + "mypy" +] +test = [ + "pytest", + "pytest-cov", + "requests", + "tqdm" +] + +[tool.pdm.scripts.download_lib] +call = "script.download_lib:download_lib" +help = "Download mediainfo library" + +[tool.pdm.scripts.lint] +composite = [ + "pre-commit run --all-files" +] +help = "Check code style against linters" + +[tool.pdm.scripts.post_install] +composite = [ + "pre-commit install", + "download_lib" +] +help = "Install the pre-commit hook" + +[tool.pdm.scripts.test] +cmd = "pytest tests --cov=src --cov-report=xml --cov-report=html" +help = "Run tests with coverage" + +[tool.ruff] +extend-ignore = ["B018", "B019", "PGH003", "B028", "RUF001"] +extend-select = [ + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "PGH", # pygrep-hooks + "RUF", # ruff + "W", # pycodestyle + "YTT" # flake8-2020 +] +fixable = ["ALL"] +line-length = 120 + +[tool.ruff.format] +indent-style = "space" +line-ending = "auto" +quote-style = "double" +skip-magic-trailing-comma = false + +[tool.ruff.isort] +combine-as-imports = true +known-first-party = ["pdm"] + +[tool.ruff.mccabe] +max-complexity = 10 diff --git a/script/download_lib.py b/script/download_lib.py new file mode 100644 index 0000000..dd734b0 --- /dev/null +++ b/script/download_lib.py @@ -0,0 +1,78 @@ +# download lib from https://github.com/TensoRaws/py-mediainfo/releases/tag/lib +import hashlib +import os +import sys +from pathlib import Path +from typing import Dict, List + +import requests +import tqdm + +projectPATH = Path(__file__).resolve().parent.parent.resolve() +libPATH = projectPATH / "pymediainfo" + +# lib name, url and hash +lib_dict: Dict[str, List[str]] = { + "libmediainfo.0.dylib": [ + "https://github.com/TensoRaws/py-mediainfo/releases/download/lib/libmediainfo.0.dylib", + "19eca636459bd2745b45f9e1842f54b1e5560bd90b87273259fa9cd2323afb71", + ], + "MediaInfo.dll": [ + "https://github.com/TensoRaws/py-mediainfo/releases/download/lib/MediaInfo.dll", + "35bc8cc3e334c95ff9597d5ad81ada726f82e5abc3c26cb28cf18067aa0e7cbd", + ], +} + + +def get_file_sha256(file_path: str, blocksize: int = 1 << 20) -> str: + sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + while True: + data = f.read(blocksize) + if not data: + break + sha256.update(data) + return sha256.hexdigest() + + +def download_lib() -> None: + if sys.platform == "win32": + lib_name = "MediaInfo.dll" + elif sys.platform == "darwin": + lib_name = "libmediainfo.0.dylib" + else: + print("Linux, install libmediainfo-dev from package manager!") + return + + lib_url, lib_hash = lib_dict[lib_name] + lib_path = libPATH / lib_name + + if lib_path.exists(): + if get_file_sha256(str(lib_path)) == lib_hash: + print(f"{lib_name} exists, skip download.") + return + + print(f"Downloading {lib_name}...") + response = requests.get(lib_url, stream=True) + response.raise_for_status() + with open(lib_path, "wb") as f: + for chunk in tqdm.tqdm(response.iter_content(chunk_size=8192)): + f.write(chunk) + + if get_file_sha256(str(lib_path)) != lib_hash: + print("Download failed, sha256 mismatch.") + lib_path.unlink() + return + + print(f"Downloaded {lib_name} successfully.") + + +if __name__ == "__main__": + # get all model files sha256 + for root, _, files in os.walk(libPATH): + for file in files: + if not file.endswith(".so") and not file.endswith(".dylib") and not file.endswith(".dll"): + continue + file_path = os.path.join(root, file) + name = os.path.basename(file_path) + print(f"{name}: {get_file_sha256(file_path)}") diff --git a/tests/data/aac_he_v2.aac b/tests/data/aac_he_v2.aac new file mode 100644 index 0000000..ee4cb1f Binary files /dev/null and b/tests/data/aac_he_v2.aac differ diff --git "a/tests/data/accentu\303\251.txt" "b/tests/data/accentu\303\251.txt" new file mode 100644 index 0000000..9f4b6d8 --- /dev/null +++ "b/tests/data/accentu\303\251.txt" @@ -0,0 +1 @@ +This is a test file diff --git a/tests/data/empty.gif b/tests/data/empty.gif new file mode 100644 index 0000000..3c51d74 Binary files /dev/null and b/tests/data/empty.gif differ diff --git a/tests/data/invalid.xml b/tests/data/invalid.xml new file mode 100644 index 0000000..97f6ed7 --- /dev/null +++ b/tests/data/invalid.xml @@ -0,0 +1,215 @@ + + + + + +260 +1 +General +General +0 +1 +1 +1 +Digital Video +Digital Video +DV +English +PCM +PCM +PCM +English +TimeCode +TimeCode +English +credits.mov +credits.mov +mov +MPEG-4 +MPEG-4 +mp4 m4v m4a m4p 3gpp 3gp 3gpp2 3g2 k3g jpm jpx mqv ismv isma f4v +QuickTime +qt +http://www.apple.com/quicktime/download/standalone.html +MPEG-4 +MPEG-4 +mp4 m4v m4a m4p 3gpp 3gp 3gpp2 3g2 k3g jpm jpx mqv ismv isma f4v +712816548 +680 MiB +680 MiB +680 MiB +680 MiB +679.8 MiB +593474 +9mn 53s +9mn 53s 474ms +9mn 53s +00:09:53.474 +9608731 +9 609 Kbps +194196 +190 KiB (0%) +190 KiB +190 KiB +190 KiB +189.6 KiB +190 KiB (0%) +0.00027 +UTC 2010-04-12 14:58:21 +UTC 2010-04-12 15:00:37 +UTC 2010-04-15 14:40:32 +2010-04-15 09:40:32 +Apple QuickTime +Apple QuickTime +Apple QuickTime +<>00;05;34;23 + + + + +148 +1 +Video +Video +0 +1 +1 +Digital Video +dvc +http://www.apple.com/quicktime/download/standalone.html +DV +DV +DV +Apple QuickTime DV (DVCPRO NTSC) +http://www.apple.com/quicktime/download/standalone.html +dvc +258558 +4mn 18s +4mn 18s 558ms +4mn 18s +00:04:18.558 +CBR +Constant +20874240 +20.9 Mbps +720 +720 pixels +480 +480 pixels +0.909 +0.889 +1.363 +4:3 +1.333 +4:3 +0.000 +VFR +Variable +21.744 +21.744 fps +0.111 +0.111 fps +29.970 +29.970 fps +29.970 +29.970 fps +5622 +NTSC +4:1:1 +Interlaced +Interlaced +Interlaced +Interlaced +2.778 +334768 +5mn 34s +5mn 34s 768ms +5mn 34s +00:05:34.768 +DropFrame=Yes / 24HourMax=No / IsVisual=No +0 +674640000 +643 MiB (95%) +643 MiB +643 MiB +643 MiB +643.4 MiB +643 MiB (95%) +0.94644 +en +English +UTC 2010-04-12 14:58:21 +UTC 2010-04-12 15:00:37 + + + + +129 +1 +Audio +Audio +0 +2 +2 +PCM +Little / Signed +Little +Signed +sowt +http://www.apple.com/quicktime/download/standalone.html +PCM +PCM +PCM +http://www.apple.com/quicktime/download/standalone.html +sowt +Little / Signed +Little +Signed +593474 +9mn 53s +9mn 53s 474ms +9mn 53s +00:09:53.474 +CBR +Constant +512000 +512 Kbps +1 +1 channel +32000 +32.0 KHz +18991168 +16 +16 bits +37982352 +36.2 MiB (5%) +36 MiB +36 MiB +36.2 MiB +36.22 MiB +36.2 MiB (5%) +0.05328 +en +English +UTC 2010-04-12 14:58:21 +UTC 2010-04-12 15:00:37 + + + + +51 +1 +Menu +Menu +0 +3 +3 +TimeCode +en +English +UTC 2010-04-12 15:00:37 +UTC 2010-04-12 15:00:37 + + + + diff --git a/tests/data/issue100.xml b/tests/data/issue100.xml new file mode 100644 index 0000000..6ec8500 --- /dev/null +++ b/tests/data/issue100.xml @@ -0,0 +1,25 @@ + + + + + 331 + 1 + General + General + 0 + 1 + 1 + 2 + AVC + AVC + AVC + AAC LC + AAC LC + AAC LC + RTP / RTP + RTP / RTP + RTP / RTP + English / English + + + diff --git a/tests/data/issue55.flv b/tests/data/issue55.flv new file mode 100644 index 0000000..f14022c Binary files /dev/null and b/tests/data/issue55.flv differ diff --git a/tests/data/mp3.mp3 b/tests/data/mp3.mp3 new file mode 100644 index 0000000..ea7e6d2 Binary files /dev/null and b/tests/data/mp3.mp3 differ diff --git a/tests/data/mp4-with-audio.mp4 b/tests/data/mp4-with-audio.mp4 new file mode 100644 index 0000000..1431e98 Binary files /dev/null and b/tests/data/mp4-with-audio.mp4 differ diff --git a/tests/data/mpeg4.mp4 b/tests/data/mpeg4.mp4 new file mode 100644 index 0000000..7f6eeac Binary files /dev/null and b/tests/data/mpeg4.mp4 differ diff --git a/tests/data/other_track.xml b/tests/data/other_track.xml new file mode 100644 index 0000000..96fff6c --- /dev/null +++ b/tests/data/other_track.xml @@ -0,0 +1,35 @@ + + + + + test.mxf + + + 2 + MPEG Video + Version 2 + + + 3 + PCM + + + 1-Material + Time code + MXF TC + 25.000 FPS + 00:00:00:00 + Material Package + Yes + + + 1-Source + Time code + MXF TC + 25.000 FPS + 00:00:00:00 + Source Package + Yes + + + diff --git a/tests/data/sample.mkv b/tests/data/sample.mkv new file mode 100644 index 0000000..dbfe036 Binary files /dev/null and b/tests/data/sample.mkv differ diff --git a/tests/data/sample.mp4 b/tests/data/sample.mp4 new file mode 100644 index 0000000..6b1cba8 Binary files /dev/null and b/tests/data/sample.mp4 differ diff --git a/tests/data/sample.xml b/tests/data/sample.xml new file mode 100644 index 0000000..ddefebf --- /dev/null +++ b/tests/data/sample.xml @@ -0,0 +1,197 @@ + + + + + 260 + 1 + General + General + 0 + 1 + 1 + 1 + Digital Video + Digital Video (DVCPRO HD) + DV + English + PCM + PCM + PCM + English + TimeCode + TimeCode + English + Downloads/source.mov + Downloads + source + mov + MPEG-4 + MPEG-4 + mp4 m4v m4a m4p 3gpp 3gp 3gpp2 3g2 k3g jpm jpx mqv ismv isma f4v + QuickTime + qt + http://www.apple.com/quicktime/download/standalone.html + MPEG-4 + MPEG-4 + mp4 m4v m4a m4p 3gpp 3gp 3gpp2 3g2 k3g jpm jpx mqv ismv isma f4v + 365132611 + 348 MiB + 348 MiB + 348 MiB + 348 MiB + 348.2 MiB + 61394 + 1mn 1s + 1mn 1s 394ms + 1mn 1s + 00:01:01.394 + 47578930 + 47.6 Mbps + 64835 + 63.3 KiB (0%) + 63 KiB + 63 KiB + 63.3 KiB + 63.32 KiB + 63.3 KiB (0%) + 0.00018 + UTC 2010-03-22 14:47:44 + UTC 2010-03-22 14:48:21 + UTC 2010-03-22 18:56:55 + 2010-03-22 13:56:55 + Apple QuickTime + Apple QuickTime + Apple QuickTime + 1C8E7037-D348-4981-9CD3-D60AFEE7FC1C + + + 148 + 1 + Video + Video + 0 + 1 + 1 + Digital Video + dvhp + DVCPRO HD + http://www.apple.com/quicktime/download/standalone.html + DV + DV + dvhp + 61394 + 1mn 1s + 1mn 1s 394ms + 1mn 1s + 00:01:01.394 + CBR + Constant + 46033920 + 46.0 Mbps + 960 + 960 pixels + 720 + 720 pixels + 1.333 + 1.778 + 16:9 + 0.000 / 0.000 + CFR + Constant + 23.976 + 23.976 fps + 1472 + NTSC + 4:1:1 + Interlaced + Interlaced + Interlaced + Interlaced + 2.778 + 3600000 + 1h 0mn + 1h 0mn 0s 0ms + 1h 0mn + 01:00:00.000 + DropFrame=No / 24HourMax=No / IsVisual=No + 0 + 353280000 + 337 MiB (97%) + 337 MiB + 337 MiB + 337 MiB + 336.9 MiB + 337 MiB (97%) + 0.96754 + en + English + UTC 2010-03-22 14:47:44 + UTC 2010-03-22 14:48:21 + + + 129 + 1 + Audio + Audio + 0 + 2 + 2 + PCM + Little / Signed + Little + Signed + sowt + http://www.apple.com/quicktime/download/standalone.html + PCM + PCM + PCM + http://www.apple.com/quicktime/download/standalone.html + sowt + Little / Signed + Little + Signed + 61394 + 1mn 1s + 1mn 1s 394ms + 1mn 1s + 00:01:01.394 + CBR + Constant + 1536000 + 1 536 Kbps + 2 + 2 channels + 48000 + 48.0 KHz + 2946912 + 16 + 16 bits + 11787776 + 11.2 MiB (3%) + 11 MiB + 11 MiB + 11.2 MiB + 11.24 MiB + 11.2 MiB (3%) + 0.03228 + en + English + UTC 2010-03-22 14:47:44 + UTC 2010-03-22 14:48:21 + + + 51 + 1 + Menu + Menu + 0 + 3 + 3 + TimeCode + en + English + UTC 2010-03-22 14:48:21 + UTC 2010-03-22 14:48:21 + + + diff --git a/tests/data/sample_with_cover.mp3 b/tests/data/sample_with_cover.mp3 new file mode 100644 index 0000000..07a7988 Binary files /dev/null and b/tests/data/sample_with_cover.mp3 differ diff --git a/tests/data/vbr_requires_parsespeed_1.mp4 b/tests/data/vbr_requires_parsespeed_1.mp4 new file mode 100644 index 0000000..bdf644e Binary files /dev/null and b/tests/data/vbr_requires_parsespeed_1.mp4 differ diff --git a/tests/test_pymediainfo.py b/tests/test_pymediainfo.py new file mode 100644 index 0000000..371be5e --- /dev/null +++ b/tests/test_pymediainfo.py @@ -0,0 +1,435 @@ +# pylint: disable=missing-module-docstring, missing-class-docstring, missing-function-docstring, +# pylint: disable=protected-access + +import functools +import http.server +import json +import os +import pathlib +import pickle +import sys +import tempfile +import threading +import unittest +import xml + +import pytest + +from pymediainfo import MediaInfo + +data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") +test_media_files = [ + "sample.mkv", + "sample.mp4", + "sample_with_cover.mp3", + "mpeg4.mp4", + "mp3.mp3", + "mp4-with-audio.mp4", +] + + +def _get_library_version(): + lib, handle, lib_version_str, lib_version = MediaInfo._get_library() + lib.MediaInfo_Close(handle) + lib.MediaInfo_Delete(handle) + return lib_version_str, lib_version + + +class MediaInfoTest(unittest.TestCase): + def setUp(self): + with open(os.path.join(data_dir, "sample.xml"), "r", encoding="utf-8") as f: + self.xml_data = f.read() + self.media_info = MediaInfo(self.xml_data) + + def test_populate_tracks(self): + self.assertEqual(4, len(self.media_info.tracks)) + + def test_valid_video_track(self): + for track in self.media_info.tracks: + if track.track_type == "Video": + self.assertEqual("DV", track.codec) + self.assertEqual("Interlaced", track.scan_type) + break + + def test_track_integer_attributes(self): + for track in self.media_info.tracks: + if track.track_type == "Audio": + self.assertTrue(isinstance(track.duration, int)) + self.assertTrue(isinstance(track.bit_rate, int)) + self.assertTrue(isinstance(track.sampling_rate, int)) + break + + def test_track_other_attributes(self): + general_tracks = [track for track in self.media_info.tracks if track.track_type == "General"] + general_track = general_tracks[0] + self.assertEqual(5, len(general_track.other_file_size)) + self.assertEqual(["1mn 1s", "1mn 1s 394ms", "1mn 1s", "00:01:01.394"], general_track.other_duration) + + def test_track_existing_other_attributes(self): + with open(os.path.join(data_dir, "issue100.xml"), encoding="utf-8") as f: + media_info = MediaInfo(f.read()) + general_tracks = [track for track in media_info.tracks if track.track_type == "General"] + general_track = general_tracks[0] + self.assertEqual(general_track.other_format_list, "RTP / RTP") + + def test_load_mediainfo_from_string(self): + self.assertEqual(4, len(self.media_info.tracks)) + + def test_getting_attribute_that_doesnot_exist(self): + self.assertTrue(self.media_info.tracks[0].does_not_exist is None) + + +class MediaInfoInvalidXMLTest(unittest.TestCase): + def setUp(self): + with open(os.path.join(data_dir, "invalid.xml"), "r", encoding="utf-8") as f: + self.xml_data = f.read() + + def test_parse_invalid_xml(self): + self.assertRaises(xml.etree.ElementTree.ParseError, MediaInfo, self.xml_data) + + +class MediaInfoLibraryTest(unittest.TestCase): + def setUp(self): + self.media_info = MediaInfo.parse(os.path.join(data_dir, "sample.mp4")) + self.non_full_mi = MediaInfo.parse(os.path.join(data_dir, "sample.mp4"), full=False) + + def test_can_parse_true(self): + self.assertTrue(MediaInfo.can_parse()) + + def test_track_count(self): + self.assertEqual(len(self.media_info.tracks), 3) + + def test_track_types(self): + self.assertEqual(self.media_info.tracks[1].track_type, "Video") + self.assertEqual(self.media_info.tracks[2].track_type, "Audio") + + def test_track_details(self): + self.assertEqual(self.media_info.tracks[1].format, "AVC") + self.assertEqual(self.media_info.tracks[2].format, "AAC") + self.assertEqual(self.media_info.tracks[1].duration, 958) + self.assertEqual(self.media_info.tracks[2].duration, 980) + + def test_full_option(self): + self.assertEqual(self.media_info.tracks[0].footersize, "59") + self.assertEqual(self.non_full_mi.tracks[0].footersize, None) + + def test_raises_on_nonexistent_library(self): + with tempfile.TemporaryDirectory() as tmp_dir: + nonexistent_library = os.path.join(tmp_dir, "nonexistent-libmediainfo.so") + with pytest.raises(OSError) as exc: + MediaInfo.parse(os.path.join(data_dir, "sample.mp4"), library_file=nonexistent_library) + assert rf"Failed to load library from {nonexistent_library}" in str(exc.value) + + +class MediaInfoFileLikeTest(unittest.TestCase): + def test_can_parse(self): + with open(os.path.join(data_dir, "sample.mp4"), "rb") as f: + MediaInfo.parse(f) + + def test_raises_on_text_mode_even_with_text(self): + with open(os.path.join(data_dir, "sample.xml"), encoding="utf-8") as f: + self.assertRaises(ValueError, MediaInfo.parse, f) + + def test_raises_on_text_mode(self): + with open(os.path.join(data_dir, "sample.mkv"), encoding="utf-8") as f: + self.assertRaises(ValueError, MediaInfo.parse, f) + + +class MediaInfoUnicodeXMLTest(unittest.TestCase): + def setUp(self): + self.media_info = MediaInfo.parse(os.path.join(data_dir, "sample.mkv")) + + def test_parse_file_with_unicode_tags(self): + self.assertEqual( + self.media_info.tracks[0].title, + "Dès Noël où un zéphyr haï me vêt de glaçons " + "würmiens je dîne d’exquis rôtis de bœuf au kir à " + "l’aÿ d’âge mûr & cætera !", + ) + + +class MediaInfoUnicodeFileNameTest(unittest.TestCase): + def setUp(self): + self.media_info = MediaInfo.parse(os.path.join(data_dir, "accentué.txt")) + + def test_parse_unicode_file(self): + self.assertEqual(len(self.media_info.tracks), 1) + + +@pytest.mark.skipif( + sys.version_info < (3, 7) or sys.platform == "win32", + reason="SimpleHTTPRequestHandler's 'directory' argument was added in Python 3.7", +) +class MediaInfoURLTest(unittest.TestCase): + def setUp(self): + HandlerClass = functools.partial( # pylint: disable=invalid-name + http.server.SimpleHTTPRequestHandler, + directory=data_dir, + ) + # Pick a random port so that parallel tests (e.g. via 'tox -p') do not clash + self.httpd = http.server.HTTPServer(("", 0), HandlerClass) + port = self.httpd.socket.getsockname()[1] + self.url = f"http://127.0.0.1:{port}/sample.mkv" + threading.Thread(target=self.httpd.serve_forever).start() + + def tearDown(self): + self.httpd.shutdown() + self.httpd.server_close() + + def test_parse_url(self): + media_info = MediaInfo.parse(self.url) + self.assertEqual(len(media_info.tracks), 3) + + +class MediaInfoPathlibTest(unittest.TestCase): + def test_parse_pathlib_path(self): + path = pathlib.Path(data_dir) / "sample.mp4" + media_info = MediaInfo.parse(path) + self.assertEqual(len(media_info.tracks), 3) + + def test_parse_non_existent_path_pathlib(self): + path = pathlib.Path(data_dir) / "this file does not exist" + self.assertRaises(FileNotFoundError, MediaInfo.parse, path) + + +class MediaInfoFilenameTypesTest(unittest.TestCase): + def test_normalize_filename_str(self): + path = os.path.join(data_dir, "test.txt") + filename = MediaInfo._normalize_filename(path) + self.assertEqual(filename, path) + + def test_normalize_filename_pathlib(self): + path = pathlib.Path(data_dir, "test.txt") + filename = MediaInfo._normalize_filename(path) + self.assertEqual(filename, os.path.join(data_dir, "test.txt")) + + def test_normalize_filename_pathlike(self): + class PathLikeObject(os.PathLike): # pylint: disable=too-few-public-methods + def __fspath__(self): + return os.path.join(data_dir, "test.txt") + + path = PathLikeObject() + filename = MediaInfo._normalize_filename(path) + self.assertEqual(filename, os.path.join(data_dir, "test.txt")) + + def test_normalize_filename_url(self): + filename = MediaInfo._normalize_filename("https://localhost") + self.assertEqual(filename, "https://localhost") + + +class MediaInfoTestParseNonExistentFile(unittest.TestCase): + def test_parse_non_existent_path(self): + path = os.path.join(data_dir, "this file does not exist") + self.assertRaises(FileNotFoundError, MediaInfo.parse, path) + + +class MediaInfoCoverDataTest(unittest.TestCase): + def setUp(self): + self.cover_mi = MediaInfo.parse(os.path.join(data_dir, "sample_with_cover.mp3"), cover_data=True) + self.no_cover_mi = MediaInfo.parse(os.path.join(data_dir, "sample_with_cover.mp3")) + + def test_parse_cover_data(self): + self.assertEqual( + self.cover_mi.tracks[0].cover_data, + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAAAAA" + "AAAQCEeRdzAAAADUlEQVR4nGP4x8DwHwAE/AH+QSRCQgAAAABJRU5ErkJggg==", + ) + + def test_parse_no_cover_data(self): + lib_version_str, lib_version = _get_library_version() + if lib_version < (18, 3): + pytest.skip( + "The Cover_Data option is not supported by this library version " + "(v{} detected, v18.03 required)".format(lib_version_str) + ) + self.assertEqual(self.no_cover_mi.tracks[0].cover_data, None) + + +class MediaInfoTrackParsingTest(unittest.TestCase): + def test_track_parsing(self): + media_info = MediaInfo.parse(os.path.join(data_dir, "issue55.flv")) + self.assertEqual(len(media_info.tracks), 2) + + +class MediaInfoRuntimeErrorTest(unittest.TestCase): + def test_parse_invalid_url(self): + # This is the easiest way to cause a parsing error + # since non-existent files return a different exception + self.assertRaises(RuntimeError, MediaInfo.parse, "unsupportedscheme://") + + +class MediaInfoSlowParseTest(unittest.TestCase): + def setUp(self): + self.media_info = MediaInfo.parse(os.path.join(data_dir, "vbr_requires_parsespeed_1.mp4"), parse_speed=1) + + def test_slow_parse_speed(self): + self.assertEqual(self.media_info.tracks[2].stream_size, "3353 / 45") + + +class MediaInfoEqTest(unittest.TestCase): + def setUp(self): + self.mp3_mi = MediaInfo.parse(os.path.join(data_dir, "sample_with_cover.mp3")) + self.mp3_other_mi = MediaInfo.parse(os.path.join(data_dir, "sample_with_cover.mp3")) + self.mp4_mi = MediaInfo.parse(os.path.join(data_dir, "sample.mp4")) + + def test_eq(self): + self.assertEqual(self.mp3_mi.tracks[0], self.mp3_other_mi.tracks[0]) + self.assertEqual(self.mp3_mi, self.mp3_other_mi) + self.assertNotEqual(self.mp3_mi.tracks[0], self.mp4_mi.tracks[0]) + self.assertNotEqual(self.mp3_mi, self.mp4_mi) + + def test_pickle_unpickle(self): + pickled_track = pickle.dumps(self.mp4_mi.tracks[0]) + self.assertEqual(self.mp4_mi.tracks[0], pickle.loads(pickled_track)) + pickled_mi = pickle.dumps(self.mp4_mi) + self.assertEqual(self.mp4_mi, pickle.loads(pickled_mi)) + + +class MediaInfoLegacyStreamDisplayTest(unittest.TestCase): + def setUp(self): + self.media_info = MediaInfo.parse(os.path.join(data_dir, "aac_he_v2.aac")) + self.legacy_mi = MediaInfo.parse(os.path.join(data_dir, "aac_he_v2.aac"), legacy_stream_display=True) + + def test_legacy_stream_display(self): + self.assertEqual(self.media_info.tracks[1].channel_s, 2) + self.assertEqual(self.legacy_mi.tracks[1].channel_s, "2 / 1 / 1") + + +class MediaInfoOptionsTest(unittest.TestCase): + def setUp(self): + lib_version_str, lib_version = _get_library_version() + if lib_version < (19, 9): + pytest.skip( + "The Reset option is not supported by this library version " "(v{} detected, v19.09 required)".format( + lib_version_str + ) + ) + self.raw_language_mi = MediaInfo.parse( + os.path.join(data_dir, "sample.mkv"), + mediainfo_options={"Language": "raw"}, + ) + # Parsing the file without the custom options afterwards + # allows us to check that the "Reset" option worked + # https://github.com/MediaArea/MediaInfoLib/issues/1128 + self.normal_mi = MediaInfo.parse( + os.path.join(data_dir, "sample.mkv"), + ) + + def test_mediainfo_options(self): + self.assertEqual(self.normal_mi.tracks[1].other_language[0], "English") + self.assertEqual(self.raw_language_mi.tracks[1].language, "en") + + +# Unittests can't be parametrized +# https://github.com/pytest-dev/pytest/issues/541 +@pytest.mark.parametrize("test_file", test_media_files) +def test_thread_safety(test_file): + lib_version_str, lib_version = _get_library_version() + if lib_version < (20, 3): + pytest.skip( + "This version of the library is not thread-safe " "(v{} detected, v20.03 required)".format(lib_version_str) + ) + expected_result = MediaInfo.parse(os.path.join(data_dir, test_file)) + results = [] + lock = threading.Lock() + + def target(): + try: + result = MediaInfo.parse(os.path.join(data_dir, test_file)) + with lock: + results.append(result) + except Exception: # pylint: disable=broad-except + pass + + threads = [] + thread_count = 100 + for _ in range(thread_count): + thread = threading.Thread(target=target) + thread.start() + threads.append(thread) + for thread in threads: + thread.join() + # Each thread should have produced a result + assert len(results) == thread_count + for res in results: + # Test dicts first because they will show a diff + # in case they don't match + assert res.to_data() == expected_result.to_data() + assert res == expected_result + + +@pytest.mark.parametrize("test_file", test_media_files) +def test_filelike_returns_the_same(test_file): + filename = os.path.join(data_dir, test_file) + mi_from_filename = MediaInfo.parse(filename) + with open(filename, "rb") as f: + mi_from_file = MediaInfo.parse(f) + assert len(mi_from_file.tracks) == len(mi_from_filename.tracks) + for track_from_file, track_from_filename in zip(mi_from_file.tracks, mi_from_filename.tracks): + # The General track will differ, typically not giving the file name + if track_from_file.track_type != "General": + # Test dicts first because they will produce a diff + assert track_from_file.to_data() == track_from_filename.to_data() + assert track_from_file == track_from_filename + + +class MediaInfoOutputTest(unittest.TestCase): + def test_text_output(self): + media_info = MediaInfo.parse(os.path.join(data_dir, "sample.mp4"), output="") + self.assertRegex(media_info, r"Stream size\s+: 373836\b") + + def test_json_output(self): + lib_version_str, lib_version = _get_library_version() + if lib_version < (18, 3): + pytest.skip( + "This version of the library does not support JSON output " "(v{} detected, v18.03 required)".format( + lib_version_str + ) + ) + media_info = MediaInfo.parse(os.path.join(data_dir, "sample.mp4"), output="JSON") + parsed = json.loads(media_info) + self.assertEqual(parsed["media"]["track"][0]["FileSize"], "404567") + + def test_parameter_output(self): + media_info = MediaInfo.parse(os.path.join(data_dir, "sample.mp4"), output="General;%FileSize%") + self.assertEqual(media_info, "404567") + + +class MediaInfoTrackShortcutsTests(unittest.TestCase): + def setUp(self): + self.mi_audio = MediaInfo.parse(os.path.join(data_dir, "sample.mp4")) + self.mi_text = MediaInfo.parse(os.path.join(data_dir, "sample.mkv")) + self.mi_image = MediaInfo.parse(os.path.join(data_dir, "empty.gif")) + with open(os.path.join(data_dir, "other_track.xml"), encoding="utf-8") as f: + self.mi_other = MediaInfo(f.read()) + + def test_empty_list(self): + self.assertEqual(self.mi_audio.text_tracks, []) + + def test_general_tracks(self): + self.assertEqual(len(self.mi_audio.general_tracks), 1) + self.assertIsNotNone(self.mi_audio.general_tracks[0].file_name) + + def test_video_tracks(self): + self.assertEqual(len(self.mi_audio.video_tracks), 1) + self.assertIsNotNone(self.mi_audio.video_tracks[0].display_aspect_ratio) + + def test_audio_tracks(self): + self.assertEqual(len(self.mi_audio.audio_tracks), 1) + self.assertIsNotNone(self.mi_audio.audio_tracks[0].sampling_rate) + + def test_text_tracks(self): + self.assertEqual(len(self.mi_text.text_tracks), 1) + self.assertEqual(self.mi_text.text_tracks[0].kind_of_stream, "Text") + + def test_other_tracks(self): + self.assertEqual(len(self.mi_other.other_tracks), 2) + self.assertEqual(self.mi_other.other_tracks[0].type, "Time code") + + def test_image_tracks(self): + self.assertEqual(len(self.mi_image.image_tracks), 1) + self.assertEqual(self.mi_image.image_tracks[0].width, 1) + + def test_menu_tracks(self): + self.assertEqual(len(self.mi_text.menu_tracks), 1) + self.assertEqual(self.mi_text.menu_tracks[0].kind_of_stream, "Menu")