diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..600fb9f --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,75 @@ +name: CI + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ${{ matrix.os }} + env: + DISPLAY: :0 + PYTEST_ADDOPTS: "--color=yes" + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + python: [ "3.11","3.12" ] + defaults: + run: + shell: bash + + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + #- name: Setup macOS PATH + # if: runner.os == 'macOS' + # run: | + # echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: ~/.cache + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + + - name: Install project + run: poetry install --no-interaction + + - name: Check format with Black + run: | + poetry run black --check . + + - name: Typecheck with mypy + run: | + poetry run mypy + + - name: Run tests + run: | + source $VENV + poetry run pytest diff --git a/.gitignore b/.gitignore index eeb60cc..6133544 100644 --- a/.gitignore +++ b/.gitignore @@ -231,3 +231,5 @@ pyrightconfig.json test audio .vscode +files +.idea diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2a00972 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,69 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +fail_fast: false + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-merge-conflict + - id: check-json + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: mixed-line-ending + - id: check-vcs-permalinks + - id: check-shebang-scripts-are-executable + - id: trailing-whitespace + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-check-mock-methods + - id: python-no-log-warn + - id: python-use-type-annotations + - id: rst-directive-colons + - id: rst-inline-touching-normal + + - repo: https://github.com/hadialqattan/pycln + rev: v2.4.0 + hooks: + - id: pycln + args: [ --all ] + + - repo: https://github.com/psf/black + rev: 23.12.1 + hooks: + - id: black + args: [ --safe ] + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort (python) + language_version: "3.12" + args: [ "--profile", "black" ] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + + - repo: https://github.com/python-poetry/poetry + rev: 1.7.0 + hooks: + - id: poetry-check + - id: poetry-lock + - id: poetry-export + args: [ "--without", "dev", "-f", "requirements.txt", "-o", "requirements.txt" ] + verbose: true + - id: poetry-install diff --git a/BAET/AppArgs.py b/BAET/AppArgs.py deleted file mode 100644 index 086f48e..0000000 --- a/BAET/AppArgs.py +++ /dev/null @@ -1,124 +0,0 @@ -import argparse -import re -from pathlib import Path -from re import Pattern -from typing import Any, Callable, cast - -from pydantic import ( - BaseModel, - BeforeValidator, - ConfigDict, - DirectoryPath, - Field, - ValidationInfo, - field_validator, -) -from typing_extensions import Annotated - - -class AppArgs(BaseModel): - """Application commandline arguments. - - Raises: - ValueError: The provided path is not a directory. - - Returns: - DirectoryPath: Validated directory path. - """ - - model_config = ConfigDict(frozen=True) - - input_dir: DirectoryPath = Field(...) - output_dir: DirectoryPath = Field(...) - overwrite_existing: bool = Field(...) - fallback_freq: Annotated[int, Field(gt=0)] = Field(...) - fast: bool = Field(...) - no_output_subdirs: bool = Field(...) - include: Pattern | None = Field(...) - exclude: Pattern | None = Field(...) - - @field_validator("include", mode="before") - @classmethod - def validate_include_nonempty(cls, v: str): - if not v or not str.strip(v): - return ".*" - return v - - @field_validator("exclude", mode="before") - @classmethod - def validate_exclude_nonempty(cls, v: str): - if not v or not str.strip(v): - return None - return v - - @field_validator("include", "exclude", mode="before") - @classmethod - def compile_to_pattern(cls, v: str): - if not v: - return None - if isinstance(v, str): - return re.compile(v) - else: - return v - - -def GetArgs() -> AppArgs: - cwd = str(Path.cwd()) - - parser = argparse.ArgumentParser( - prog="Bulk Audio Extract Tool (BAET)", - description="Extract audio from a directory of videos using FFMPEG", - epilog="Created by: Phillip Smith 2023", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument( - "--fast", - default=False, - action="store_true", - help="only extracting the first 10 seconds of audio for each file", - ) - - parser.add_argument( - "--overwrite-existing", - default=False, - action="store_true", - help="overwrite a file if it already exists", - ) - - parser.add_argument( - "--no-output-subdirs", - default=False, - action="store_true", - help="do not create subdirectories for each video's extracted audio tracks in the output directory", - ) - - parser.add_argument( - "--include", - default=None, - metavar="REGEX", - help="only include files that match a regex pattern", - ) - - parser.add_argument( - "--exclude", - default=None, - metavar="REGEX", - help="exclude files that match a regex pattern", - ) - - parser.add_argument( - "--fallback-freq", - default=48000, - metavar="FREQUENCY", - help="------------------", - ) - - parser.add_argument( - "-i", "--input-dir", default=cwd, type=Path, help="source location" - ) - parser.add_argument( - "-o", "--output-dir", default=cwd, type=Path, help="destination location" - ) - - args = parser.parse_args() - return AppArgs.model_validate(vars(args)) diff --git a/BAET/AudioExtractor.py b/BAET/AudioExtractor.py deleted file mode 100644 index f4f84c8..0000000 --- a/BAET/AudioExtractor.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path - -from BAET.AppArgs import AppArgs - - -class AudioExtractor: - def __init__( - self, - file_name: str, - app_args: AppArgs, - ): - if not file_name: - pass - self.file = (app_args.input_dir / file_name).resolve(strict=True) - self.app_args = app_args diff --git a/BAET/__init__.py b/BAET/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index f37dc0e..c6044c1 100644 --- a/README.md +++ b/README.md @@ -1 +1,24 @@ -# Bulk Audio Extract Tool \ No newline at end of file +# Bulk Audio Extract Tool + +## About + +## Install +Depending on your platform, to call python, you may need to use the command `python` or `python3`. +Typing `python3 --version` or `python --version` should display the currently installed python environment your PATH. +For the remainder of this document, replace instances of `python3` with the appropriate alias on your machine. + +To install the requirements: +```bash +python3 -m pip install -r requirements.txt +``` + +Then, call `./main.py --help` to see application information. + +## Add to Path + +On Linux/MacOS, locate your `~/.bashrc` or `~/.zshrc` file and edit it, adding: +```bash +BAET_PATH="/path/to/BulkAudioExtractTool/main.py" +alias baet="python3 $(BAET_PATH)" +``` +Restart your terminal and enter `baet --help`. The application's help screen should now show from any directory. diff --git a/main.py b/main.py deleted file mode 100644 index e2257c4..0000000 --- a/main.py +++ /dev/null @@ -1,63 +0,0 @@ -import sys -from pathlib import Path - -import ffmpeg -from rich import print as pprint - -from BAET.AppArgs import GetArgs - - -def get_files_in_dir(dir_path: Path, extension: list[str]) -> list[Path]: - """Get all files in a directory with the specified extension. - - Args: - dir_path (Path): Path to directory. - extension (str): File extension to filter by. - - Returns: - list[Path]: List of files in the directory with the specified extension. - """ - return [file for file in dir_path.iterdir() if file.suffix in extension] - - -def main(): - args = GetArgs() - - files = get_files_in_dir(args.input_dir, [".mp4", ".mkv"]) - - indexes = set() - for file in files: - pprint(file) - - try: - probe = ffmpeg.probe(file) - except ffmpeg.Error as e: - print(e.stderr, file=sys.stderr) - sys.exit(1) - - outputs = [] - audio_streams = [ - stream["index"] - for stream in probe["streams"] - if "codec_type" in stream and stream["codec_type"] == "audio" - ] - - if not audio_streams: - print("No audio streams found", file=sys.stderr) - sys.exit(1) - - input = ffmpeg.input(str(file)) - - for i in audio_streams: - stream = input[f"a:{i}"] - outputs.append(stream.output(f"output{i}.aac", acodec="copy")) - - output = ffmpeg.merge_outputs(*outputs) - cmd = " ".join(output.compile()) - print(cmd) - - # output.run() - - -if __name__ == "__main__": - main() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..63d7be7 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,27 @@ +[mypy] +strict = True +python_version = 3.12 +files = src + +install_types = true +ignore_missing_imports = True +cache_fine_grained = True +warn_unused_ignores = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +disallow_untyped_decorators = False +disallow_subclassing_any = False +warn_return_any = True +implicit_optional = False +strict_optional = True +warn_unreachable = True + +[mypy-BAET._aliases] +disable_error_code = valid-type + +[mypy-BAET._constants] +disable_error_code = valid-type + +[mypy-BAET.progress_status] +disable_error_code = valid-type diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..5791a28 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,826 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "astroid" +version = "3.0.2" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "astroid-3.0.2-py3-none-any.whl", hash = "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c"}, + {file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"}, +] + +[[package]] +name = "asyncio" +version = "3.4.3" +description = "reference implementation of PEP 3156" +optional = false +python-versions = "*" +files = [ + {file = "asyncio-3.4.3-cp33-none-win32.whl", hash = "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de"}, + {file = "asyncio-3.4.3-cp33-none-win_amd64.whl", hash = "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c"}, + {file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"}, + {file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"}, +] + +[[package]] +name = "bidict" +version = "0.22.1" +description = "The bidirectional mapping library for Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "bidict-0.22.1-py3-none-any.whl", hash = "sha256:6ef212238eb884b664f28da76f33f1d28b260f665fc737b413b287d5487d1e7b"}, + {file = "bidict-0.22.1.tar.gz", hash = "sha256:1e0f7f74e4860e6d0943a05d4134c63a2fad86f3d4732fb265bd79e4e856d81d"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton"] +lint = ["pre-commit"] +test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "pytest-xdist", "sortedcollections", "sortedcontainers", "sphinx"] + +[[package]] +name = "black" +version = "23.12.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +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 = "dill" +version = "0.3.7" +description = "serialize all of Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +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 = "ffmpeg-python" +version = "0.2.0" +description = "Python bindings for FFmpeg - with complex filtering support" +optional = false +python-versions = "*" +files = [ + {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"}, + {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"}, +] + +[package.dependencies] +future = "*" + +[package.extras] +dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"] + +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "future" +version = "0.18.3" +description = "Clean single-source support for Python 3 and 2" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, +] + +[[package]] +name = "identify" +version = "2.5.33" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +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 = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "more-itertools" +version = "10.1.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"}, + {file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"}, +] + +[[package]] +name = "mypy" +version = "1.8.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +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" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +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.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.6.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pydantic" +version = "2.5.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.14.6" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.14.6" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pylint" +version = "3.0.3" +description = "python code static checker" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "pylint-3.0.3-py3-none-any.whl", hash = "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810"}, + {file = "pylint-3.0.3.tar.gz", hash = "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b"}, +] + +[package.dependencies] +astroid = ">=3.0.1,<=3.1.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, +] +isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +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-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {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 = "rich" +version = "13.7.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, + {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rich-argparse" +version = "1.4.0" +description = "Rich help formatters for argparse and optparse" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rich_argparse-1.4.0-py3-none-any.whl", hash = "sha256:68b263d3628d07b1d27cfe6ad896da2f5a5583ee2ba226aeeb24459840023b38"}, + {file = "rich_argparse-1.4.0.tar.gz", hash = "sha256:c275f34ea3afe36aec6342c2a2298893104b5650528941fb53c21067276dba19"}, +] + +[package.dependencies] +rich = ">=11.0.0" + +[[package]] +name = "setuptools" +version = "69.0.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "tomlkit" +version = "0.12.3" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, +] + +[[package]] +name = "types-docutils" +version = "0.20.0.3" +description = "Typing stubs for docutils" +optional = false +python-versions = "*" +files = [ + {file = "types-docutils-0.20.0.3.tar.gz", hash = "sha256:4928e790f42b99d5833990f99c8dd9fa9f16825f6ed30380ca981846d36870cd"}, + {file = "types_docutils-0.20.0.3-py3-none-any.whl", hash = "sha256:a930150d8e01a9170f9bca489f46808ddebccdd8bc1e47c07968a77e49fb9321"}, +] + +[[package]] +name = "types-pygments" +version = "2.17.0.0" +description = "Typing stubs for Pygments" +optional = false +python-versions = ">=3.7" +files = [ + {file = "types-Pygments-2.17.0.0.tar.gz", hash = "sha256:4241c5f1b7448e559cd820143a564cf10de626a95ab10e2daa463449d16864e7"}, + {file = "types_Pygments-2.17.0.0-py3-none-any.whl", hash = "sha256:83c33c89037f433fd5315b1abf80f5cb8b589b2d0549444d7f63824c628142c7"}, +] + +[package.dependencies] +types-docutils = "*" +types-setuptools = "*" + +[[package]] +name = "types-setuptools" +version = "69.0.0.0" +description = "Typing stubs for setuptools" +optional = false +python-versions = ">=3.7" +files = [ + {file = "types-setuptools-69.0.0.0.tar.gz", hash = "sha256:b0a06219f628c6527b2f8ce770a4f47550e00d3e8c3ad83e2dc31bc6e6eda95d"}, + {file = "types_setuptools-69.0.0.0-py3-none-any.whl", hash = "sha256:8c86195bae2ad81e6dea900a570fe9d64a59dbce2b11cc63c046b03246ea77bf"}, +] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.11" +content-hash = "61dc16e8051da6ede25203efd6c51da28888958fb313ad06f5f20694df78ac16" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..931848f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,63 @@ +[tool.poetry] +name = "BAET" +version = "0.1.1" +description = "" +authors = ["TimeTravelPenguin "] +readme = "README.md" +packages = [{ include = "BAET", from = "src" }] +classifiers = [ + "Environment :: Console", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Typing :: Typed", +] + +[project.urls] +Homepage = "https://github.com/TimeTravelPenguin/BulkAudioExtractTool" + +[tool.poetry.scripts] +baet = "BAET.__main__:main" + +[tool.poetry.dev-dependencies] +black = "*" +isort = "*" +mypy = "*" +pre-commit = "^3.6.0" +pylint = "*" +pytest = "^7.4.4" +types-pygments = "^2.17.0.0" + +[tool.poetry.dependencies] +python = ">=3.11" +asyncio = "^3.4.3" +bidict = "^0.22.1" +ffmpeg-python = "^0.2.0" +more-itertools = "^10.1.0" +pydantic = "^2.5.2" +rich = "^13.7.0" +rich-argparse = "^1.4.0" + +[tool.black] +line-length = 120 + +[tool.isort] +profile = "black" +line_length = 120 + +[tool.mypy] +mypy_path = "src" + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.poetry-dynamic-versioning] +enable = true +metadata = false +vcs = "git" + +[build-system] +requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] +build-backend = "poetry_dynamic_versioning.backend" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..676bd65 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,146 @@ +annotated-types==0.6.0 ; python_version >= "3.11" \ + --hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \ + --hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d +asyncio==3.4.3 ; python_version >= "3.11" \ + --hash=sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41 \ + --hash=sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de \ + --hash=sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c \ + --hash=sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d +bidict==0.22.1 ; python_version >= "3.11" \ + --hash=sha256:1e0f7f74e4860e6d0943a05d4134c63a2fad86f3d4732fb265bd79e4e856d81d \ + --hash=sha256:6ef212238eb884b664f28da76f33f1d28b260f665fc737b413b287d5487d1e7b +ffmpeg-python==0.2.0 ; python_version >= "3.11" \ + --hash=sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127 \ + --hash=sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5 +future==0.18.3 ; python_version >= "3.11" \ + --hash=sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307 +markdown-it-py==3.0.0 ; python_version >= "3.11" \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb +mdurl==0.1.2 ; python_version >= "3.11" \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba +more-itertools==10.1.0 ; python_version >= "3.11" \ + --hash=sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a \ + --hash=sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6 +pydantic-core==2.14.6 ; python_version >= "3.11" \ + --hash=sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556 \ + --hash=sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e \ + --hash=sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411 \ + --hash=sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245 \ + --hash=sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c \ + --hash=sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66 \ + --hash=sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd \ + --hash=sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d \ + --hash=sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b \ + --hash=sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06 \ + --hash=sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948 \ + --hash=sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341 \ + --hash=sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0 \ + --hash=sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f \ + --hash=sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a \ + --hash=sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2 \ + --hash=sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51 \ + --hash=sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80 \ + --hash=sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8 \ + --hash=sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d \ + --hash=sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8 \ + --hash=sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb \ + --hash=sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590 \ + --hash=sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87 \ + --hash=sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534 \ + --hash=sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b \ + --hash=sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145 \ + --hash=sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba \ + --hash=sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b \ + --hash=sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2 \ + --hash=sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e \ + --hash=sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052 \ + --hash=sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622 \ + --hash=sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab \ + --hash=sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b \ + --hash=sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66 \ + --hash=sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e \ + --hash=sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4 \ + --hash=sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e \ + --hash=sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec \ + --hash=sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c \ + --hash=sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed \ + --hash=sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937 \ + --hash=sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f \ + --hash=sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9 \ + --hash=sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4 \ + --hash=sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96 \ + --hash=sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277 \ + --hash=sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23 \ + --hash=sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7 \ + --hash=sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b \ + --hash=sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91 \ + --hash=sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d \ + --hash=sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e \ + --hash=sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1 \ + --hash=sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2 \ + --hash=sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160 \ + --hash=sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9 \ + --hash=sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670 \ + --hash=sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7 \ + --hash=sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c \ + --hash=sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb \ + --hash=sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42 \ + --hash=sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d \ + --hash=sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8 \ + --hash=sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1 \ + --hash=sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6 \ + --hash=sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8 \ + --hash=sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf \ + --hash=sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e \ + --hash=sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a \ + --hash=sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9 \ + --hash=sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1 \ + --hash=sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40 \ + --hash=sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2 \ + --hash=sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d \ + --hash=sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f \ + --hash=sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f \ + --hash=sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af \ + --hash=sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7 \ + --hash=sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda \ + --hash=sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a \ + --hash=sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95 \ + --hash=sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0 \ + --hash=sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60 \ + --hash=sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149 \ + --hash=sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975 \ + --hash=sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4 \ + --hash=sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe \ + --hash=sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94 \ + --hash=sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03 \ + --hash=sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c \ + --hash=sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b \ + --hash=sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a \ + --hash=sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24 \ + --hash=sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391 \ + --hash=sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c \ + --hash=sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab \ + --hash=sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd \ + --hash=sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786 \ + --hash=sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08 \ + --hash=sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8 \ + --hash=sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6 \ + --hash=sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0 \ + --hash=sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421 +pydantic==2.5.3 ; python_version >= "3.11" \ + --hash=sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a \ + --hash=sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4 +pygments==2.17.2 ; python_version >= "3.11" \ + --hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \ + --hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367 +rich-argparse==1.4.0 ; python_version >= "3.11" \ + --hash=sha256:68b263d3628d07b1d27cfe6ad896da2f5a5583ee2ba226aeeb24459840023b38 \ + --hash=sha256:c275f34ea3afe36aec6342c2a2298893104b5650528941fb53c21067276dba19 +rich==13.7.0 ; python_version >= "3.11" \ + --hash=sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa \ + --hash=sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235 +typing-extensions==4.9.0 ; python_version >= "3.11" \ + --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ + --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd diff --git a/src/BAET/__init__.py b/src/BAET/__init__.py new file mode 100644 index 0000000..4220a00 --- /dev/null +++ b/src/BAET/__init__.py @@ -0,0 +1,5 @@ +from ._console import app_console +from ._logging import create_logger +from ._theme import app_theme + +__all__ = ["app_console", "create_logger", "app_theme"] diff --git a/src/BAET/__main__.py b/src/BAET/__main__.py new file mode 100644 index 0000000..63887fa --- /dev/null +++ b/src/BAET/__main__.py @@ -0,0 +1,35 @@ +import logging +import sys + +import rich +from rich.live import Live +from rich.traceback import install + +from BAET import app_console, create_logger +from BAET.app_args import get_args +from BAET.extract import MultiTrackAudioBulkExtractor + +install(show_locals=True) + +VIDEO_EXTENSIONS = [".mp4", ".mkv"] + + +def main() -> None: + args = get_args() + + if args.debug_options.print_args: + rich.print(args) + sys.exit(0) + + if not args.debug_options.logging: + logging.disable(logging.CRITICAL) + + logger = create_logger() + logger.info("Building extractor jobs") + extractor = MultiTrackAudioBulkExtractor(args) + + with Live(extractor, console=app_console): + logger.info("Running jobs") + extractor.run_synchronously() + + sys.exit(0) diff --git a/src/BAET/_aliases.py b/src/BAET/_aliases.py new file mode 100644 index 0000000..1ebbe23 --- /dev/null +++ b/src/BAET/_aliases.py @@ -0,0 +1,18 @@ +from collections.abc import Mapping +from typing import Any, TypeAlias + +from bidict import BidirectionalMapping +from ffmpeg import Stream +from rich.progress import TaskID + +# Numbers +Millisecond: TypeAlias = int | float + +# FFmpeg +StreamIndex: TypeAlias = int +AudioStream: TypeAlias = dict[str, Any] + +# Mappings +IndexedOutputs: TypeAlias = Mapping[StreamIndex, Stream] +IndexedAudioStream: TypeAlias = Mapping[StreamIndex, AudioStream] +StreamTaskBiMap: TypeAlias = BidirectionalMapping[StreamIndex, TaskID] diff --git a/src/BAET/_console.py b/src/BAET/_console.py new file mode 100644 index 0000000..50f8c9b --- /dev/null +++ b/src/BAET/_console.py @@ -0,0 +1,6 @@ +from rich.console import Console + +from BAET._theme import app_theme + +app_console = Console(theme=app_theme) +error_console = Console(stderr=True, style="bold red", theme=app_theme) diff --git a/src/BAET/_constants.py b/src/BAET/_constants.py new file mode 100644 index 0000000..7a53143 --- /dev/null +++ b/src/BAET/_constants.py @@ -0,0 +1,5 @@ +from typing import Literal, Sequence, TypeAlias + +VideoExtension: TypeAlias = Literal[".mp4", ".mkv"] + +VIDEO_EXTENSIONS: Sequence[VideoExtension] = [".mp4", ".mkv"] diff --git a/src/BAET/_logging.py b/src/BAET/_logging.py new file mode 100644 index 0000000..9233a21 --- /dev/null +++ b/src/BAET/_logging.py @@ -0,0 +1,30 @@ +import inspect +import logging +from logging import Logger + +from rich.logging import RichHandler + +from BAET._console import app_console + +rich_handler = RichHandler(rich_tracebacks=True, console=app_console) +logging.basicConfig( + level=logging.INFO, + format="%(threadName)s: %(message)s", + datefmt="[%X]", + handlers=[rich_handler], +) + +app_logger = logging.getLogger("app_logger") + + +def create_logger() -> Logger: + frame = inspect.stack()[1] + module = inspect.getmodule(frame[0]) + + if module is None: + raise RuntimeError("Could not inspect module") + + module_name = module.__name__ + + logger = app_logger.getChild(module_name) + return logger diff --git a/src/BAET/_metadata.py b/src/BAET/_metadata.py new file mode 100644 index 0000000..5ea2ce2 --- /dev/null +++ b/src/BAET/_metadata.py @@ -0,0 +1,5 @@ +import importlib.metadata + + +def app_version() -> str: + return importlib.metadata.version("BAET") diff --git a/src/BAET/_theme.py b/src/BAET/_theme.py new file mode 100644 index 0000000..d64efad --- /dev/null +++ b/src/BAET/_theme.py @@ -0,0 +1,16 @@ +from rich.theme import Theme + +app_theme = Theme( + { + "app.version": "italic bright_cyan", + "argparse.arg_default": "dim italic", + "argparse.arg_default_parens": "dim", + "argparse.arg_default_value": "not italic bold dim", + "argparse.help_keyword": "bold blue", + "argparse.debug_todo": "reverse bold indian_red", + "status.waiting": "dim bright_white", + "status.running": "bright_cyan", + "status.completed": "bright_green", + "status.error": "bright_red", + } +) diff --git a/src/BAET/app_args.py b/src/BAET/app_args.py new file mode 100644 index 0000000..db4cab1 --- /dev/null +++ b/src/BAET/app_args.py @@ -0,0 +1,355 @@ +import argparse +import re +from argparse import ArgumentParser +from pathlib import Path +from re import Pattern + +from pydantic import BaseModel, ConfigDict, DirectoryPath, Field, field_validator +from rich.console import Console, ConsoleOptions, RenderResult +from rich.markdown import Markdown +from rich.padding import Padding +from rich.table import Table +from rich.terminal_theme import DIMMED_MONOKAI +from rich.text import Text +from rich_argparse import HelpPreviewAction, RichHelpFormatter +from typing_extensions import Annotated + +from BAET._console import app_console +from BAET._metadata import app_version + +file_type_pattern = re.compile(r"^\.?(\w+)$") + + +class InputFilters(BaseModel): + include: Pattern[str] = Field(...) + exclude: Pattern[str] | None = Field(...) + + @field_validator("include", mode="before") + @classmethod + def validate_include_nonempty(cls, v: str) -> Pattern[str]: + if v is None or not v.strip(): + return re.compile(".*") + return re.compile(v) + + @field_validator("exclude", mode="before") + @classmethod + def validate_exclude_nonempty(cls, v: str) -> Pattern[str] | None: + if v is None or not v.strip(): + return None + return re.compile(v) + + +class OutputConfigurationOptions(BaseModel): + output_streams_separately: bool = Field(...) + overwrite_existing: bool = Field(...) + no_output_subdirs: bool = Field(...) + acodec: str = Field(...) + fallback_sample_rate: Annotated[int, Field(gt=0)] = Field(...) + file_type: str = Field(...) + + @field_validator("file_type", mode="before") + @classmethod + def validate_file_type(cls, v: str) -> str: + matched = file_type_pattern.match(v) + if matched: + return matched.group(1) + raise ValueError(f"Invalid file type: {v}") + + +class DebugOptions(BaseModel): + logging: bool = Field(...) + dry_run: bool = Field(...) + trim: Annotated[int, Field(gt=0)] | None = Field(...) + print_args: bool = Field(...) + show_ffmpeg_cmd: bool = Field(...) + run_synchronously: bool = Field(...) + + +class AppDescription: + @staticmethod + def __rich_console__(console: Console, options: ConsoleOptions) -> RenderResult: + yield Markdown("# Bulk Audio Extract Tool (src)") + yield "Extract audio from a directory of videos using FFMPEG.\n" + + website_link = "https://github.com/TimeTravelPenguin/BulkAudioExtractTool" + desc_kvps = [ + ( + Padding(Text("App name:", justify="right"), (0, 5, 0, 0)), + Text( + "Bulk Audio Extract Tool (src)", + style="argparse.prog", + justify="left", + ), + ), + ( + Padding(Text("Version:", justify="right"), (0, 5, 0, 0)), + Text(app_version(), style="app.version", justify="left"), + ), + ( + Padding(Text("Author:", justify="right"), (0, 5, 0, 0)), + Text("Phillip Smith", style="bright_yellow", justify="left"), + ), + ( + Padding(Text("Website:", justify="right"), (0, 5, 0, 0)), + Text( + website_link, + style=f"underline blue link {website_link}", + justify="left", + ), + ), + ] + + grid = Table.grid(expand=True) + grid.add_column(justify="left") + grid.add_column(justify="right") + for key, value in desc_kvps: + grid.add_row(key, value) + + yield grid + + +def new_empty_argparser() -> ArgumentParser: + def get_formatter(prog: str) -> RichHelpFormatter: + return RichHelpFormatter(prog, max_help_position=40, console=app_console) + + # todo: use console protocol https://rich.readthedocs.io/en/stable/protocol.html#console-protocol + description = AppDescription() + + RichHelpFormatter.highlights.append( + r"(?P\((?PDefault: (?P.*))\))" + ) + + RichHelpFormatter.highlights.append(r"(?Pffmpeg|ffprobe)") + RichHelpFormatter.highlights.append(r"(?P\[TODO\])") + + return argparse.ArgumentParser( + prog="Bulk Audio Extract Tool (src)", + description=description, # type: ignore + epilog=Markdown( # type: ignore + "Phillip Smith, 2023", + justify="right", + style="argparse.prog", + ), + formatter_class=get_formatter, + ) + + +class AppArgs(BaseModel): + """Application commandline arguments. + + Raises: + ValueError: The provided path is not a directory. + + Returns: + DirectoryPath: Validated directory path. + """ + + model_config = ConfigDict(frozen=True, from_attributes=True) + + input_dir: DirectoryPath = Field(...) + output_dir: DirectoryPath = Field(...) + input_filters: InputFilters = Field(...) + output_configuration: OutputConfigurationOptions = Field(...) + debug_options: DebugOptions = Field(...) + + +def get_args() -> AppArgs: + parser = new_empty_argparser() + + parser.add_argument( + "--version", + action="version", + version=f"[argparse.prog]%(prog)s[/] version [i]{app_version()}[/]", + ) + + io_group = parser.add_argument_group( + "Input/Output", + "Options to control the source and destination directories of input and output files.", + ) + + io_group.add_argument( + "-i", + "--input-dir", + action="store", + type=DirectoryPath, + metavar="INPUT_DIR", + required=True, + help="Source directory.", + ) + + io_group.add_argument( + "-o", + "--output-dir", + default=None, + action="store", + type=Path, + help="Destination directory. Default is set to the input directory. To use the current directory, " + 'use [blue]"."[/]. (Default: None)', + ) + + query_group = parser.add_argument_group( + title="Input Filter Configuration", + description="Configure how the application includes and excludes files to process.", + ) + + query_group.add_argument( + "--include", + default=None, + metavar="REGEX", + help='[TODO] If provided, only include files that match a regex pattern. (Default: ".*")', + ) + + query_group.add_argument( + "--exclude", + default=None, + metavar="REGEX", + help="[TODO] If provided, exclude files that match a regex pattern. (Default: None)", + ) + + output_group = parser.add_argument_group( + title="Output Configuration", + description="Override the default output behavior of the application.", + ) + + output_group.add_argument( + "--output-streams-separately", + "--sep", + default=False, + action="store_true", + help="[TODO] When set, individual commands are given to [blue]ffmpeg[/] to export each stream. Otherwise, " + "a single command is given to ffmpeg to export all streams. This latter option will result in all files " + "appearing in the directory at once, and so any errors may result in a loss of data. Setting this flag " + "may be useful when experiencing errors. (Default: False)", + ) + + output_group.add_argument( + "--overwrite-existing", + "--overwrite", + default=False, + action="store_true", + help="Overwrite a file if it already exists. (Default: False)", + ) + + output_group.add_argument( + "--no-output-subdirs", + default=False, + action="store_true", + help="Do not create subdirectories for each video's extracted audio tracks in the output directory. " + "(Default: True)", + ) + + output_group.add_argument( + "--acodec", + default="pcm_s16le", + metavar="CODEC", + help='[TODO] The audio codec to use when extracting audio. (Default: "pcm_s16le")', + ) + + output_group.add_argument( + "--fallback-sample-rate", + default=48000, + metavar="RATE", + help="[TODO] The sample rate to use if it cannot be determined via [blue]ffprobe[/]. (Default: 48000)", + ) + + output_group.add_argument( + "--file-type", + default="wav", + metavar="EXT", + help='[TODO] The file type to use for the extracted audio. (Default: "wav")', + ) + + debug_group = parser.add_argument_group( + "Debugging", + "Options to help debug the application.", + ) + + debug_group.add_argument( + "--run-synchronously", + "--sync", + default=False, + action="store_true", + help="[TODO] Run each each job in order. This should reduce the CPU workload, but may increase runtime. A " + "'job' is per file input, regardless of whether ffmpeg commands are merged (see: " + "`--output-streams-separately`). (Default: False)", + ) + + debug_group.add_argument( + "--logging", + default=False, + action="store_true", + help="[TODO] Show the logging of application execution. (Default: False)", + ) + + debug_group.add_argument( + "--print-args", + default=False, + action="store_true", + help="Print the parsed arguments and exit. (Default: False)", + ) + + debug_group.add_argument( + "--dry-run", + default=False, + action="store_true", + help="Run the program without actually extracting any audio. (Default: False)", + ) + + debug_group.add_argument( + "--show-ffmpeg-cmd", + "--cmds", + default=False, + action="store_true", + help="[TODO] Print to the console the generated ffmpeg command. (Default: False)", + ) + + debug_group.add_argument( + "--trim-short", + default=None, + type=int, + dest="trim", + help="[TODO] Trim the audio to the specified number of seconds. This is useful for testing. (Default: None)", + ) + + parser.add_argument( + "--generate-help-preview", + action=HelpPreviewAction, + path="help-preview.svg", # (optional) or "help-preview.html" or "help-preview.txt" + export_kwds={"theme": DIMMED_MONOKAI}, # (optional) keywords passed to console.save_... methods + help=argparse.SUPPRESS, + ) + + args = parser.parse_args() + + input_filters = InputFilters(include=args.include, exclude=args.exclude) + + output_config = OutputConfigurationOptions( + output_streams_separately=args.output_streams_separately, + overwrite_existing=args.overwrite_existing, + no_output_subdirs=args.no_output_subdirs, + acodec=args.acodec, + fallback_sample_rate=args.fallback_sample_rate, + file_type=args.file_type, + ) + + debug_options = DebugOptions( + logging=args.logging, + dry_run=args.dry_run, + trim=args.trim, + print_args=args.print_args, + show_ffmpeg_cmd=args.show_ffmpeg_cmd, + run_synchronously=args.run_synchronously, + ) + + app_args: AppArgs = AppArgs.model_validate( + { + "input_dir": args.input_dir.expanduser(), + "output_dir": args.output_dir or args.input_dir.expanduser(), + "input_filters": input_filters, + "output_configuration": output_config, + "debug_options": debug_options, + }, + strict=True, + ) + + return app_args diff --git a/src/BAET/extract.py b/src/BAET/extract.py new file mode 100644 index 0000000..0b61529 --- /dev/null +++ b/src/BAET/extract.py @@ -0,0 +1,149 @@ +import contextlib +from collections.abc import Generator, Iterator, MutableMapping +from pathlib import Path +from typing import Any + +import ffmpeg +from ffmpeg import Stream +from rich.console import Console, ConsoleOptions, Group, RenderResult +from rich.padding import Padding +from rich.table import Table + +from ._aliases import AudioStream +from ._console import error_console +from ._constants import VIDEO_EXTENSIONS +from ._logging import create_logger +from .app_args import AppArgs, InputFilters, OutputConfigurationOptions +from .job_progress import FFmpegJobProgress +from .jobs import FFmpegJob + +logger = create_logger() + + +@contextlib.contextmanager +def probe_audio_streams(file: Path) -> Iterator[list[AudioStream]]: + try: + logger.info('Probing file "%s"', file) + probe = ffmpeg.probe(file) + + audio_streams = sorted( + [stream for stream in probe["streams"] if "codec_type" in stream and stream["codec_type"] == "audio"], + key=lambda stream: stream["index"], + ) + + if not audio_streams: + raise ValueError("No audio streams found") + + logger.info("Found %d audio streams", len(audio_streams)) + yield audio_streams + + except (ffmpeg.Error, ValueError) as e: + logger.critical("%s: %s", type(e).__name__, e) + error_console.print_exception() + raise e + + +class FileSourceDirectory: + def __init__(self, directory: Path, filters: InputFilters): + if not directory.is_dir(): + raise NotADirectoryError(directory) # FIXME: Is this right? + + self._directory = directory.resolve(strict=True) + self._filters = filters + + def __iter__(self) -> Generator[Path, Any, None]: + for file in self._directory.iterdir(): + if not file.is_file(): + continue + + # TODO: This should be moved. + # Rather than checking a global variable, it should be provided somehow. + # Perhaps via command line args. + if file.suffix not in VIDEO_EXTENSIONS: + continue + + if self._filters.include.match(file.name): + if self._filters.exclude is None: + yield file + elif self._filters.exclude.match(file.name): + yield file + + +class MultitrackAudioBulkExtractorJobs: + def __init__( + self, + input_dir: Path, + output_dir: Path, + filters: InputFilters, + output_configuration: OutputConfigurationOptions, + ): + self._input_dir = input_dir + self._output_dir = output_dir + self._filters = filters + self._output_configuration = output_configuration + self._file_source_directory = FileSourceDirectory(input_dir, filters) + + def _create_output_filepath(self, file: Path, stream_index: int) -> Path: + filename = Path(f"{file.stem}_track{stream_index}.{self._output_configuration.file_type}") + + out_path = self._output_dir if self._output_configuration.no_output_subdirs else self._output_dir / file.stem + + out_path.mkdir(parents=True, exist_ok=True) + return out_path / filename + + def build_job(self, file: Path) -> FFmpegJob: + audio_streams: list[AudioStream] = [] + indexed_outputs: MutableMapping[int, Stream] = {} + + with probe_audio_streams(file) as streams: + for idx, stream in enumerate(streams): + audio_streams.append(stream) + + ffmpeg_input = ffmpeg.input(str(file)) + stream_index = stream["index"] + output_path = self._create_output_filepath(file, stream_index) + sample_rate = stream.get( + "sample_rate", + self._output_configuration.fallback_sample_rate, + ) + + ffmpeg_output = ffmpeg.output( + ffmpeg_input[f"a:{idx}"], + str(output_path), + acodec=self._output_configuration.acodec, + audio_bitrate=sample_rate, + ) + + if self._output_configuration.overwrite_existing: + ffmpeg_output = ffmpeg.overwrite_output(ffmpeg_output) + + indexed_outputs[stream_index] = ffmpeg_output.global_args("-progress", "-", "-nostats") + + return FFmpegJob(file, audio_streams, indexed_outputs) + + def __iter__(self) -> Generator[FFmpegJob, Any, None]: + yield from map(self.build_job, self._file_source_directory) + + +class MultiTrackAudioBulkExtractor: + def __init__(self, app_args: AppArgs) -> None: + self._extractor_jobs = MultitrackAudioBulkExtractorJobs( + app_args.input_dir, + app_args.output_dir, + app_args.input_filters, + app_args.output_configuration, + ) + + self.display = Table.grid() + self._logger = logger + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield self.display + + def run_synchronously(self) -> None: + job_progresses = [FFmpegJobProgress(job) for job in self._extractor_jobs] + self.display.add_row(Padding(Group(*job_progresses), pad=(1, 2))) + + for progress in job_progresses: + self._logger.info(f"Processing input file '{progress.job.input_file}'") + progress.start() diff --git a/src/BAET/job_progress.py b/src/BAET/job_progress.py new file mode 100644 index 0000000..9ba75fa --- /dev/null +++ b/src/BAET/job_progress.py @@ -0,0 +1,123 @@ +import ffmpeg +from bidict import MutableBidirectionalMapping, bidict +from rich.console import Console, ConsoleOptions, ConsoleRenderable, Group, RenderResult +from rich.padding import Padding +from rich.progress import BarColumn, Progress, TaskID, TextColumn, TimeElapsedColumn, TimeRemainingColumn + +from ._aliases import StreamTaskBiMap +from ._console import app_console, error_console +from ._logging import create_logger +from .jobs import FFmpegJob + +logger = create_logger() + + +class FFmpegJobProgress(ConsoleRenderable): + # TODO: Need mediator to consumer/producer printing + def __init__(self, job: FFmpegJob): + self.job = job + + bar_blue = "#5079AF" + bar_yellow = "#CAAF39" + + self._overall_progress = Progress( + TextColumn("Progress for {task.fields[filename]}"), + BarColumn( + complete_style=bar_blue, + finished_style="green", + pulse_style=bar_yellow, + ), + TextColumn("Completed {task.completed} of {task.total}"), + TimeElapsedColumn(), + TimeRemainingColumn(), + console=app_console, + ) + + self._overall_progress_task = self._overall_progress.add_task( + "Waiting...", + start=False, + filename=job.input_file.name, + total=len(self.job.audio_streams), + ) + + self._stream_task_progress = Progress( + TextColumn("Audio Stream {task.fields[stream_index]}"), + BarColumn( + complete_style=bar_blue, + finished_style="green", + pulse_style=bar_yellow, + ), + TextColumn("{task.fields[status]}"), + TimeElapsedColumn(), + TimeRemainingColumn(), + console=app_console, + ) + + self._stream_task_bimap: StreamTaskBiMap = bidict() + + stream_task_bimap: MutableBidirectionalMapping[int, TaskID] = bidict() + for stream in self.job.audio_streams: + stream_index = stream["index"] + + task = self._stream_task_progress.add_task( + "Waiting...", + start=False, + total=self.job.stream_duration_ms(stream), + stream_index=stream_index, + status="Waiting", + ) + + stream_task_bimap[stream_index] = task + + self._stream_task_bimap = stream_task_bimap + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield Group( + self._overall_progress, + Padding(self._stream_task_progress, (1, 0, 1, 5)), + ) + + def _run_task(self, task: TaskID) -> None: + stream_index = self._stream_task_bimap.inverse[task] + + logger.info(f"Extracting audio stream {stream_index} of {self.job.input_file.name}") + + output = self.job.indexed_outputs[stream_index] + + proc = ffmpeg.run_async( + output, + pipe_stdout=True, + pipe_stderr=True, + ) + + try: + with proc as p: + for line in p.stdout: + decoded = line.decode("utf-8").strip() + if "out_time_ms" in decoded: + val = decoded.split("=", 1)[1] + self._stream_task_progress.update( + task, + completed=float(val), + ) + + if p.returncode != 0: + raise RuntimeError(p.stderr.read().decode("utf-8")) + except RuntimeError as e: + logger.critical("%s: %s", type(e).__name__, e) + error_console.print_exception() + raise e + + def start(self) -> None: + self._overall_progress.start_task(self._overall_progress_task) + for task in self._stream_task_bimap.values(): + self._stream_task_progress.start_task(task) + self._stream_task_progress.update(task, status="Working") + + self._run_task(task) + + self._stream_task_progress.update(task, status="Done") + self._stream_task_progress.stop_task(task) + self._overall_progress.advance(self._overall_progress_task, advance=1) + + self._overall_progress.stop_task(self._overall_progress_task) diff --git a/src/BAET/jobs.py b/src/BAET/jobs.py new file mode 100644 index 0000000..e0a6869 --- /dev/null +++ b/src/BAET/jobs.py @@ -0,0 +1,44 @@ +from fractions import Fraction +from pathlib import Path + +from more_itertools import first_true + +from ._aliases import AudioStream, IndexedAudioStream, IndexedOutputs, Millisecond, StreamIndex + + +class FFmpegJob: + def __init__( + self, + input_file: Path, + audio_streams: list[AudioStream], + indexed_outputs: IndexedOutputs, + ): + self.input_file: Path = input_file + self.indexed_outputs: IndexedOutputs = indexed_outputs + self.audio_streams = audio_streams + + indexed_audio_streams = {} + for stream in audio_streams: + indexed_audio_streams[stream["index"]] = stream + self.indexed_audio_streams: IndexedAudioStream = indexed_audio_streams + + # TODO: Do we need this? + self.durations_ms_dict: dict[StreamIndex, Millisecond] = { + stream["index"]: self.stream_duration_ms(stream) for stream in audio_streams + } + + @classmethod + def stream_duration_ms(cls, stream: AudioStream) -> Millisecond: + return 1_000_000 * float(stream["duration_ts"]) * float(Fraction(stream["time_base"])) + + def stream(self, index: StreamIndex) -> AudioStream: + stream: AudioStream | None = first_true( + self.audio_streams, + default=None, + pred=lambda st: st["index"] == index, # type: ignore + ) + + if stream is None: + raise IndexError(f'Stream with index "{index}" not found') + + return stream diff --git a/src/BAET/progress_list.py b/src/BAET/progress_list.py new file mode 100644 index 0000000..52219d3 --- /dev/null +++ b/src/BAET/progress_list.py @@ -0,0 +1,46 @@ +from rich.console import Console +from rich.progress import Progress, TextColumn + +from .progress_status import ProgressStatus +from .progress_style import ProgressStyle + + +class ProgressCheckList: + def __init__( + self, + waiting_description: str, + running_description: str, + completed_description: str, + error_description: str, + progress_style: ProgressStyle | None = None, + console: Console | None = None, + ): + self.descriptions: dict[ProgressStatus, str] = { + ProgressStatus.Waiting: waiting_description, + ProgressStatus.Running: running_description, + ProgressStatus.Completed: completed_description, + ProgressStatus.Error: error_description, + } + + self.progress_style = progress_style or ProgressStyle() + + self.overall_progress = Progress( + TextColumn("{task.description}"), + console=console, + ) + + self.overall_progress_task = self.overall_progress.add_task( + waiting_description, + total=None, + start=False, + ) + + def add_item( + self, + waiting_description: str, + running_description: str, + completed_description: str, + error_description: str, + ) -> None: + # TODO + pass diff --git a/src/BAET/progress_status.py b/src/BAET/progress_status.py new file mode 100644 index 0000000..447d254 --- /dev/null +++ b/src/BAET/progress_status.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from enum import StrEnum +from typing import Literal, TypeAlias + +from rich.console import Console, ConsoleOptions, RenderResult +from rich.repr import Result + +ProgressStatusLiteral: TypeAlias = Literal["Waiting", "Running", "Completed", "Error"] + + +class ProgressStatus(StrEnum): + Waiting = "Waiting" + Running = "Running" + Completed = "Completed" + Error = "Error" + + def __rich_repr__(self) -> Result: + yield repr(self) + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield repr(self) + + +ProgressStatusType: TypeAlias = ProgressStatus | ProgressStatusLiteral + +if __name__ == "__main__": + import rich + + waiting = ProgressStatus("Waiting") + completed = ProgressStatus(ProgressStatus.Completed) + + rich.print(waiting) + rich.print(completed) diff --git a/src/BAET/progress_style.py b/src/BAET/progress_style.py new file mode 100644 index 0000000..8aa526e --- /dev/null +++ b/src/BAET/progress_style.py @@ -0,0 +1,132 @@ +from collections.abc import Generator, Mapping +from typing import Any + +from rich.console import Console, ConsoleOptions, RenderResult +from rich.repr import Result +from rich.style import Style +from rich.text import Text + +from BAET._theme import app_theme +from BAET.progress_status import ProgressStatus, ProgressStatusLiteral, ProgressStatusType + + +def parse_style(style: Style | str) -> Style: + if isinstance(style, Style): + return style + + return Style.parse(style) + + +class ProgressStyle: + def __init__( + self, + style_dict: dict[ProgressStatusType, str | Style] | None = None, + *, + waiting_style: str | Style | None = None, + running_style: str | Style | None = None, + completed_style: str | Style | None = None, + error_style: str | Style | None = None, + ): + default_style_dict: dict[ProgressStatus, Style] = { + key: parse_style(value or default) + for key, value, default in [ + (ProgressStatus.Waiting, waiting_style, "dim bright_white"), + (ProgressStatus.Running, running_style, "bright_cyan"), + (ProgressStatus.Completed, completed_style, "bright_green"), + (ProgressStatus.Error, error_style, "bright_red"), + ] + } + + if style_dict is not None: + parsed_styles = {ProgressStatus(key): parse_style(val) for key, val in style_dict.items()} + + default_style_dict.update(parsed_styles) + + self.style_dict: Mapping[ProgressStatus, Style] = default_style_dict + + def __call__( + self, + text: str, + status: ProgressStatus | ProgressStatusLiteral, + *args: Any, + style: Style | None = None, + **kwargs: Any, + ) -> Text: + return Text( + text, + self.style_dict[ProgressStatus(status)] + style, + *args, + **kwargs, + ) + + def __rich_repr__(self) -> Result: + yield "style_dict", self.style_dict + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + with console.capture() as capture: + console.print(self.style_dict) + yield Text.from_ansi(capture.get()) + + +if __name__ == "__main__": + from rich.console import Group, group + from rich.padding import Padding + from rich.panel import Panel + from rich.pretty import pprint + + console = Console(theme=app_theme) + + basic_style = ProgressStyle( + {"Completed": "bold green", ProgressStatus.Error: "bold red"}, + running_style="italic bold cyan", + waiting_style="underline yellow", + ) + + theme_style = ProgressStyle( + { + ProgressStatus.Waiting: app_theme.styles["status.waiting"], + ProgressStatus.Running: app_theme.styles["status.running"], + ProgressStatus.Completed: app_theme.styles["status.completed"], + ProgressStatus.Error: app_theme.styles["status.error"], + }, + ) + + @group() + def apply_styles(style: ProgressStyle, message: str) -> Generator[Text, Any, None]: + for status in ["Waiting", "Completed", "Running", "Error"]: + yield style(message.format(status), status) # type: ignore + + @group() + def make_style_group(style: ProgressStyle, message: str) -> Generator[Text, Any, None]: + with console.capture() as pretty_capture: + pprint( + {key.value: val.color.name for key, val in style.style_dict.items() if val.color}, + console=console, + expand_all=True, + indent_guides=False, + ) + with console.capture() as repr_capture: + console.print( + style, + "\n[u]Demo Application:[/]", + Padding(apply_styles(style, message), pad=(0, 0, 0, 3)), + ) + + yield Text.from_ansi(f"Input Style: {pretty_capture.get()}\n") + yield Text.from_ansi(f"Resulting ProgressStyle: {repr_capture.get()}") + + layout = Group( + Panel( + make_style_group(basic_style, "This is the style for ProgressStyle.{0}"), + title="[bold green]Basic Style", + ), + Panel( + make_style_group( + theme_style, + 'This is the style for "ProgressStyle.{0}", taken from app_theme in BAET', + ), + title="[bold green]Theme Style", + ), + ) + + console.print(layout) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..758c60d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,5 @@ +from rich.console import Console + +from BAET import app_theme + +test_console = Console(theme=app_theme) diff --git a/tests/test_progress_style.py b/tests/test_progress_style.py new file mode 100644 index 0000000..f198410 --- /dev/null +++ b/tests/test_progress_style.py @@ -0,0 +1,14 @@ +from rich.style import Style +from rich.text import Text + +from BAET.progress_status import ProgressStatus, ProgressStatusType +from BAET.progress_style import ProgressStyle + + +def test_call_applies_style() -> None: + waiting_style: dict[ProgressStatusType, str | Style] = {ProgressStatus.Running: "blue"} + input_str = "test message" + style = ProgressStyle(style_dict=waiting_style) + test_style = Style.parse(waiting_style[ProgressStatus.Running]) + + assert style(input_str, status="Running").markup == Text(input_str, style=test_style).markup