diff options
Diffstat (limited to '')
| -rw-r--r-- | .hgignore | 260 | ||||
| -rw-r--r-- | PKGBUILD/PKGBUILD | 19 | ||||
| -rw-r--r-- | patternutils/commands/patterncfg.py | 158 | ||||
| -rw-r--r-- | patternutils/commands/pjson.py | 13 | ||||
| -rw-r--r-- | patternutils/commands/pmatch.py | 94 | ||||
| -rw-r--r-- | patternutils/commands/pmediainfo.py | 100 | ||||
| -rw-r--r-- | patternutils/commands/prss.py | 10 | ||||
| -rw-r--r-- | patternutils/commands/targs.py | 138 | ||||
| -rw-r--r-- | patternutils/commands/tln.py | 77 | ||||
| -rw-r--r-- | patternutils/commands/tmv.py | 109 | ||||
| -rw-r--r-- | patternutils/config.py | 126 | ||||
| -rw-r--r-- | patternutils/match.py | 14 | ||||
| -rw-r--r-- | patternutils/template.py | 112 | ||||
| -rw-r--r-- | patternutils/types.py | 14 | ||||
| -rw-r--r-- | patternutils/utils.py | 122 | ||||
| -rw-r--r-- | poetry.lock | 335 | ||||
| -rw-r--r-- | pyproject.toml | 64 | ||||
| -rw-r--r-- | uv.lock | 274 |
18 files changed, 1342 insertions, 697 deletions
@@ -1,18 +1,254 @@ syntax: glob -*.pyc -*.json -*.egg-info -*.tar.xz -*.tar.xz.sig -*.tar.zst - -build/* -dist/* PKGBUILD/pkg/* PKGBUILD/src/* -PKGBUILD/*.zip PKGBUILD/.git/* PKGBUILD/.SRCINFO -venv/* -.mypy_cache/* +PKGBUILD/*.zip +PKGBUILD/*.tar.zst +PKGBUILD/*.tar.gz + +.pdm-build/* + +# Created by https://www.toptal.com/developers/gitignore/api/python,linux,windows,macosx,mercurial,git +# Edit at https://www.toptal.com/developers/gitignore?templates=python,linux,windows,macosx,mercurial,git + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +#!! ERROR: macosx is undefined. Use list command to see defined gitignore types !!# + +### Mercurial ### +.hg/ +.hgignore +.hgsigs +.hgsub +.hgsubstate +.hgtags + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/python,linux,windows,macosx,mercurial,git diff --git a/PKGBUILD/PKGBUILD b/PKGBUILD/PKGBUILD index 42ca51b..c4d66af 100644 --- a/PKGBUILD/PKGBUILD +++ b/PKGBUILD/PKGBUILD @@ -1,26 +1,23 @@ # Maintainer: Dennis Fink <dennis.fink@c3l.lu> pkgname=patternutils -pkgver=0.0.12 -pkgrel=1 +pkgver=0.0.13 +pkgrel=2 pkgdesc="Combine regular expressions with template engines in a powerful matter." arch=('any') url="https://projects.metalgamer.eu/patternutils" license=('GPL3') -depends=('python' 'python-click' 'python-jinja' 'python-pygments' 'python-feedparser') -source=('https://download.metalgamer.eu/patternutils/patternutils-0.0.12.tar.gz') -sha512sums=('2d39fce29eda388436f2c11da1df8016436f0441e1c77c53aa1b7fa863e7862073629ee4dfb7d082abea0b70760c2535554fec0a0bfc6cd8674f688c6f37c771') - -prepare() { - sed -i 's/==/>=/g' "$srcdir/$pkgname-$pkgver/setup.py" -} +depends=('python' 'python-click' 'python-jinja' 'python-pygments' 'python-feedparser' 'python-pymediainfo') +makedepends=(python-build python-installer python-wheel) +source=('https://download.metalgamer.eu/patternutils/patternutils-0.0.13.tar.gz') +sha512sums=('b7dd8562a14193bbe04002b0f4073f03a424633ba6effb483fac01c511dcec6fbd092120d9b4b0f728adefe273d21bec4d3cc754ce26c46a5cc06fa1f05b1043') build() { cd "$srcdir/$pkgname-$pkgver" - python setup.py build + python -m build --wheel --no-isolation } package() { cd "$srcdir/$pkgname-$pkgver" - python setup.py install --root="$pkgdir" --optimize=1 + python -m installer --destdir="$pkgdir" dist/*.whl } diff --git a/patternutils/commands/patterncfg.py b/patternutils/commands/patterncfg.py index 3421f39..5972397 100644 --- a/patternutils/commands/patterncfg.py +++ b/patternutils/commands/patterncfg.py @@ -1,11 +1,10 @@ +import itertools import os import os.path -from typing import Optional +from typing import Literal, Optional, get_args import click -from .. import config - try: from pygments import highlight # type: ignore from pygments.formatters import TerminalFormatter # type: ignore @@ -16,167 +15,190 @@ else: has_pygments = True -CURRENT_SUPPORTED_PROGRAMS = ("pmatch", "tmv", "targs", "tln") +from .. import config +from ..types import ( + PredefinedCommands, + TemplateExtensions, +) + +CURRENT_SUPPORTED_PROGRAMS: tuple[PredefinedCommands] = get_args(PredefinedCommands) @click.group(context_settings={"help_option_names": ("-h", "--help", "-?")}) def patterncfg() -> None: + """CLI tool for managing patternutils templates.""" pass -@patterncfg.command(name="init", help="initialize config folders") +@patterncfg.command(name="init", help="Initialize config folders") def init() -> None: - for dir in CURRENT_SUPPORTED_PROGRAMS: - os.makedirs(os.path.join(config.CONFIG_PATH, dir), exist_ok=True) + """Creates necessary config directories if they do not exist.""" + for directory in itertools.chain(CURRENT_SUPPORTED_PROGRAMS, ("globals",)): + os.makedirs(os.path.join(config.CONFIG_PATH, directory), exist_ok=True) -@patterncfg.command(name="list", help="list all patternutils template files") +@patterncfg.command(name="list", help="List all patternutils template files") def list() -> None: + """Lists all available template files in the config directory.""" try: - for d in os.listdir(config.CONFIG_PATH): - click.secho(d, fg="blue") - for f in os.listdir(os.path.join(config.CONFIG_PATH, d)): - click.echo(f"└── {f}") - except FileNotFoundError as e: + for directory in os.listdir(config.CONFIG_PATH): + click.secho(directory, fg="blue") + for filename in os.listdir(os.path.join(config.CONFIG_PATH, directory)): + click.echo(f"└── {filename}") + except FileNotFoundError: click.secho( - f"{e.filename} not found! Please run patterncfg init first.", + f"Config directory '{config.CONFIG_PATH}' not found! Run 'patterncfg init' first.", fg="red", err=True, ) - raise SystemExit + raise SystemExit(1) -@patterncfg.command(name="add", help="add new template file") +@patterncfg.command(name="add", help="Add a new template file") @click.option( "-t", "--template-engine", default=None, type=click.Choice(("python", "jinja2")), - help="select the template engine to use. Only applicable for tmv, targs and tln", + help="Select the template engine (only applicable for tmv, targs, tln).", ) @click.argument("command", required=True, type=click.Choice(CURRENT_SUPPORTED_PROGRAMS)) @click.argument("name") -def add(template_engine: Optional[str], command: str, name: str) -> None: +def add( + template_engine: Optional[TemplateExtensions], + command: str, + name: str, +) -> None: + """Creates a new template file for a given command.""" + fileext: TemplateExtensions | Literal["regex"] | None = template_engine if command == "pmatch": fileext = "regex" - else: - fileext = "jinja2" if template_engine == "jinja2" else "python" filename = os.path.join(config.CONFIG_PATH, command, f"{name}.{fileext}") click.edit(filename=filename) -@patterncfg.command(name="remove", help="remove template file") +@patterncfg.command(name="remove", help="Remove a template file") @click.option( "-c", "--command", default=None, type=click.Choice(CURRENT_SUPPORTED_PROGRAMS), - help="select the command from which to remove the file", + help="Specify the command from which to remove the file.", ) @click.argument("name") -def remove(command: Optional[str], name: str) -> None: - +def remove(command: Optional[PredefinedCommands], name: str) -> None: + """Removes a specified template file.""" files = config.find_templates(name, command) if not files: - click.secho("No files found!", fg="red", err=True) - raise SystemExit - elif len(files) != 1: click.secho( - "Multiple files found! Please specify the command from which to remove the file.", + f"No matching template files found for '{name}'.", fg="red", err=True + ) + + raise SystemExit(1) + + if len(files) > 1: + click.secho( + "Multiple files found! Please specify the command to remove the correct file.", fg="red", err=True, ) for f in files: click.secho(f, fg="red", err=True) - raise SystemExit - else: - os.remove(files[0]) + raise SystemExit(1) + + os.remove(files[0]) -@patterncfg.command(name="view", help="view template file") +@patterncfg.command(name="view", help="View a template file") @click.option( "-c", "--command", default=None, type=click.Choice(CURRENT_SUPPORTED_PROGRAMS), - help="select the command from which to view the file", + help="Specify the command from which to view the file.", ) @click.option( "--color/--no-color", is_flag=True, default=has_pygments, - help="Colorize output using pygments", + help="Colorize output using pygments if available.", ) @click.argument("name") -def view(name: str, command: str, color: bool) -> None: +def view(name: str, command: PredefinedCommands, color: bool) -> None: + """Displays the contents of a template file.""" + files = config.find_templates(name, command) if not files: - click.secho("No files found!", fg="red", err=True) - raise SystemExit - elif len(files) != 1: click.secho( - "Multiple files found! Please specify the command from which to view the file.", + f"No matching template files found for '{name}'.", fg="red", err=True + ) + raise SystemExit(1) + + if len(files) > 1: + click.secho( + "Multiple files found! Please specify the command to view the correct file.", fg="red", err=True, ) for f in files: click.secho(f, fg="red", err=True) - raise SystemExit - else: - filename = files[0] + raise SystemExit(1) + + filename = files[0] try: with open(filename, encoding="utf-8") as fd: text = fd.read() except FileNotFoundError: - click.secho("File not found!", fg="red", err=True) - raise SystemExit - else: - if color and has_pygments: - if filename.endswith("jinja2"): - lexer = DjangoLexer - else: - lexer = Python3Lexer - text = highlight(text, lexer(), TerminalFormatter()) - elif color and not has_pygments: + click.secho(f"File '{filename}' not found!", fg="red", err=True) + raise SystemExit(1) + + if color: + if not has_pygments: click.secho( - "Pygments is not installed!. Cannot colorize output.", + "Pygments is not installed! Cannot colorize output.", fg="red", err=True, ) - raise SystemExit - click.echo(text[:-1]) + raise SystemExit(1) + + lexer = DjangoLexer if filename.endswith("jinja2") else Python3Lexer + text = highlight(text, lexer(), TerminalFormatter()) + + click.echo(text[:-1]) -@patterncfg.command(name="edit", help="edit a template file") +@patterncfg.command(name="edit", help="Edit a template file") @click.option( "-c", "--command", default=None, type=click.Choice(CURRENT_SUPPORTED_PROGRAMS), - help="select the command from which to edit the file", + help="Specify the command from which to edit the file.", ) @click.argument("name") -def edit(name: str, command: str) -> None: +def edit(name: str, command: PredefinedCommands) -> None: + """Opens a template file in an editor.""" files = config.find_templates(name, command) if not files: - click.secho("No files found!", fg="red", err=True) - raise SystemExit - elif len(files) != 1: click.secho( - "Multiple files found! Please specify the command from which to edit the file.", + f"No matching template files found for '{name}'.", fg="red", err=True + ) + raise SystemExit(1) + + if len(files) > 1: + click.secho( + "Multiple files found! Please specify the command to edit the correct file.", fg="red", err=True, ) - for f in files: - click.secho(f, fg="red", err=True) - raise SystemExit - else: - filename = files[0] + for file in files: + click.secho(file, fg="red", err=True) + raise SystemExit(1) - click.edit(filename=filename) + click.edit(filename=files[0]) diff --git a/patternutils/commands/pjson.py b/patternutils/commands/pjson.py index 8604971..de300cb 100644 --- a/patternutils/commands/pjson.py +++ b/patternutils/commands/pjson.py @@ -1,6 +1,6 @@ -import json import re -from typing import Any, Pattern +from collections.abc import Iterable +from typing import Any, Pattern, TextIO import click @@ -67,7 +67,7 @@ def apply_regex( def pjson( key: str, regex_pattern: str, - read_from: click.File, + read_from: TextIO, stream: bool, abort_on_no_key: bool, abort_on_no_match: bool, @@ -75,7 +75,6 @@ def pjson( human_readable: bool, verbose: bool, ) -> None: - try: regex_pattern_c = re.compile(regex_pattern) except re.error as e: @@ -84,10 +83,11 @@ def pjson( click.secho(e.msg, fg="red", err=True) raise SystemExit + data: Iterable[dict[str, Any]] if stream: - data = map(utils.json_loads, read_from) # type: ignore + data = map(utils.json_loads, read_from) else: - data = utils.json_loads(read_from.read()) # type: ignore + data = utils.json_loads(read_from.read()) matches = [] @@ -95,7 +95,6 @@ def pjson( has_no_match = False has_empty_data = False for line in data: - if not line: has_empty_data = True click.secho(f"{line} has no input.", fg="red", err=True) diff --git a/patternutils/commands/pmatch.py b/patternutils/commands/pmatch.py index 3884a9d..20f71a1 100644 --- a/patternutils/commands/pmatch.py +++ b/patternutils/commands/pmatch.py @@ -1,87 +1,112 @@ import functools import itertools +import os import os.path import re -from typing import Any, Generator, Iterator, Optional, Pattern +from typing import Generator, Iterator, Optional, Pattern import click -from .. import config +from .. import config, utils from .. import match as mat -from .. import utils def apply_regex( - walk_function: Iterator[Any], + walk_function: Iterator[os.DirEntry[str]], regex_pattern: Pattern[str], match_full_path: bool, ) -> Generator[Optional[dict[str, str]], None, None]: + """Applies a regex pattern to directory entries and yields matched data. - for f in walk_function: - subject = os.path.abspath(f.path) if match_full_path else f.name + Args: + walk_function: An iterator over directory entries. + regex_pattern: A compiled regex pattern. + match_full_path: Whether to match against full absolute paths. + + Yields: + A dictionary containing match results, or None if no match is found. + """ + + for entry in walk_function: + subject = os.path.abspath(entry.path) if match_full_path else entry.name try: match = mat.apply(subject, regex_pattern) except ValueError: - click.secho(f"{subject} did not match", fg="red", err=True) + click.secho(f"'{subject}' did not match!", fg="red", err=True) yield None - else: - match["_path"] = f.path - match["_name"] = f.name - match["_abspath"] = os.path.abspath(f.path) - match["_realpath"] = os.path.realpath(f.path) - match["_relpath"] = os.path.relpath(f.path) - yield match + continue + + match.update( + { + "_path": entry.path, + "_name": entry.name, + "_abspath": os.path.abspath(entry.path), + "_realpath": os.path.realpath(entry.path), + "_relpath": os.path.relpath(entry.path), + } + ) + yield match @click.command(context_settings={"help_option_names": ("-h", "--help", "-?")}) -@click.option("-d", "--directory", multiple=True, default=["./"]) +@click.option( + "-d", + "--directory", + multiple=True, + default=["./"], + help="directories to search. Can be specified multiple times. Default: ./", +) @click.option( "-m", "--match-directories", is_flag=True, default=False, - help="search directory name also for matches", + help="Match on directories as well.", ) @click.option( "-r", "--recursive", is_flag=True, default=False, - help="search directories recursively", + help="Search directories recursively.", ) @click.option( "--abort-on-no-match", is_flag=True, default=False, - help="exit if a name does not match the INPUT_PATTERN", + help="Exit if a name does not match the INPUT_PATTERN.", ) @click.option( "--match-full-path", is_flag=True, default=False, - help="match on full absolute path instead of only the filename", + help="Match on full absolute path instead of only the filename.", ) @click.option( "-p", "--use-predefined-pattern", is_flag=True, default=False, - help="use predefined regex pattern", + help="Use predefined regex pattern.", ) @click.option( "--stream", is_flag=True, default=False, - help="Output a stream of objects instead a list of objects", + help="Output a stream of objects instead a list.", ) @click.option( "--human-readable", is_flag=True, default=False, - help="output the JSON in a human readable format", + help="Output the JSON in a human readable format.", ) @click.option( - "-v", "--verbose", is_flag=True, default=False, help="explain what is being done" + "-v", + "--verbose", + is_flag=True, + default=False, + help="Explain what is being done.", ) @click.version_option() @click.argument("regex-pattern") @@ -97,16 +122,23 @@ def pmatch( human_readable: bool, verbose: bool, ) -> None: + """Match the REGEX_PATTERN against files in specified directories and output JSON.""" if use_predefined_pattern: try: regex_pattern, _ = config.load_template(regex_pattern, "pmatch") except FileNotFoundError: - click.secho(f"Regex {regex_pattern} not found!", fg="red", err=True) - raise SystemExit + click.secho( + f"Regex pattern '{regex_pattern}' not found!", fg="red", err=True + ) + raise SystemExit(1) except RuntimeError: - click.secho("Multiple files found!", fg="red", err=True) - raise SystemExit + click.secho( + f"Multiple template files found for '{regex_pattern}'!", + fg="red", + err=True, + ) + raise SystemExit(1) try: regex_pattern_c = re.compile(regex_pattern) @@ -114,7 +146,7 @@ def pmatch( if verbose: click.secho("Regular expression could not be compiled!", fg="red", err=True) click.secho(e.msg, fg="red", err=True) - raise SystemExit + raise SystemExit(1) # Monkey patch walk function to include commandline options walk_function = functools.partial( @@ -122,13 +154,13 @@ def pmatch( ) new_walk = itertools.chain.from_iterable(map(walk_function, directory)) - has_invalid_matches = False matches = [] + has_invalid_matches = False for match in apply_regex(new_walk, regex_pattern_c, match_full_path): if match is None: has_invalid_matches = True if stream and abort_on_no_match: - raise SystemExit + raise SystemExit(1) else: if stream: click.echo(utils.json_dumps(match, human_readable)) @@ -137,7 +169,7 @@ def pmatch( if not stream: if has_invalid_matches and abort_on_no_match: - raise SystemExit + raise SystemExit(1) click.echo(utils.json_dumps(matches, human_readable)) diff --git a/patternutils/commands/pmediainfo.py b/patternutils/commands/pmediainfo.py new file mode 100644 index 0000000..35f9d4c --- /dev/null +++ b/patternutils/commands/pmediainfo.py @@ -0,0 +1,100 @@ +import functools +import itertools +import os.path +from typing import Any, Generator, Iterator, Optional + +import click +from pymediainfo import MediaInfo + +from .. import utils + + +def apply_mediainfo( + walk_function: Iterator[os.DirEntry[str]], +) -> Generator[Optional[dict[str, Any]], None, None]: + for f in walk_function: + media_info_raw = MediaInfo.parse(os.path.abspath(f)) + if len(media_info_raw.tracks) <= 1: + click.secho(f"{f.name} cannot be parsed by mediainfo", fg="red", err=True) + yield None + else: + media_info = media_info_raw.to_data() + media_info["_path"] = f.path + media_info["_name"] = f.name + media_info["_abspath"] = os.path.abspath(f.path) + media_info["_realpath"] = os.path.realpath(f.path) + media_info["_relpath"] = os.path.relpath(f.path) + yield media_info + + +@click.command(context_settings={"help_option_names": ("-h", "--help", "-?")}) +@click.option("-d", "--directory", multiple=True, default=["./"]) +@click.option( + "-r", + "--recursive", + is_flag=True, + default=False, + help="search directories recursively", +) +@click.option( + "--abort-on-unparseable", + is_flag=True, + default=False, + help="exit if a file cannot be parsed by mediainfo", +) +@click.option( + "--stream", + is_flag=True, + default=False, + help="Output a stream of objects instead a list of objects", +) +@click.option( + "--human-readable", + is_flag=True, + default=False, + help="output the JSON in a human readable format", +) +@click.option( + "-v", + "--verbose", + is_flag=True, + default=False, + help="explain what is being done", +) +@click.version_option() +def pmediainfo( + directory: list[str], + recursive: bool, + abort_on_unparseable: bool, + stream: bool, + human_readable: bool, + verbose: bool, +) -> None: + # Monkey patch walk function to include commandline options + walk_function = functools.partial( + utils.walk, recursive=recursive, match_directories=False + ) + new_walk = itertools.chain.from_iterable(map(walk_function, directory)) + + has_unparseable_files = False + parsed_data = [] + + for media_info in apply_mediainfo(new_walk): + if media_info is None: + has_unparseable_files = True + if stream and abort_on_unparseable: + raise SystemExit + else: + if stream: + click.echo(utils.json_dumps(media_info, human_readable)) + else: + parsed_data.append(media_info) + + if not stream: + if has_unparseable_files and abort_on_unparseable: + raise SystemExit + click.echo(utils.json_dumps(parsed_data, human_readable)) + + +if __name__ == "__main__": + pmediainfo() diff --git a/patternutils/commands/prss.py b/patternutils/commands/prss.py index 1ec4675..d4232ee 100644 --- a/patternutils/commands/prss.py +++ b/patternutils/commands/prss.py @@ -2,7 +2,7 @@ import functools import itertools import os import os.path -from typing import Any, Generator, Iterator, Optional +from typing import Any, Generator, Optional import click @@ -20,7 +20,6 @@ def match( walk_function: functools.partial[Generator[os.DirEntry[Any], None, None]], directory: list[str], ) -> Generator[Optional[dict[str, str]], None, None]: - direntries = [] filenames = [] @@ -90,7 +89,11 @@ def match( help="output the JSON in a human readable format", ) @click.option( - "-v", "--verbose", is_flag=True, default=False, help="explain what is being done" + "-v", + "--verbose", + is_flag=True, + default=False, + help="explain what is being done", ) @click.version_option() @click.argument("url") @@ -103,7 +106,6 @@ def prss( human_readable: bool, verbose: bool, ) -> None: - try: parsed_feed = feedparser.parse(url) except Exception: diff --git a/patternutils/commands/targs.py b/patternutils/commands/targs.py index 34cf018..a4f91e0 100644 --- a/patternutils/commands/targs.py +++ b/patternutils/commands/targs.py @@ -1,91 +1,121 @@ import functools -import json import multiprocessing -import os import shlex import subprocess -from typing import Callable +from collections.abc import Iterable +from typing import Any, Callable, TextIO import click from .. import config, template, utils +from ..types import TemplateExtensions def run_command( command: str, - run: Callable[..., None], + run: functools.partial[subprocess.CompletedProcess[str]] | Callable[..., None], shell: bool, -) -> None: - run(shlex.split(command) if not shell else command) +) -> subprocess.CompletedProcess[str] | None: + """Executes a given command using the provided run function. + + Args: + command: The command string to execute. + run: A callable function for execution (e.g., subprocess.run or print). + shell: Whether to run the command in a shell. + """ + return run(shlex.split(command) if not shell else command) @click.command(context_settings={"help_option_names": ("-h", "--help", "-?")}) -@click.option("--read-from", default="-", type=click.File("r")) @click.option( - "-s", "--shell", is_flag=True, default=False, help="run command in a shell" + "--read-from", + default="-", + type=click.File("r"), + help="Read input from file or stdin.", +) +@click.option( + "-s", + "--shell", + is_flag=True, + default=False, + help="Run command in a shell.", ) @click.option( "-t", "--template-engine", default="python", type=click.Choice(["python", "jinja2"]), - help="select the template engine to use", + help="Select the template engine to use.", ) @click.option( - "-v", "--verbose", is_flag=True, default=False, help="explain what is being done" + "-v", + "--verbose", + is_flag=True, + default=False, + help="Explain what is being done.", +) +@click.option( + "-P", + "--max-procs", + default=1, + type=click.IntRange(min=0, clamp=True), + help="Max parallel processes.", ) -@click.option("-P", "--max-procs", default=1, type=click.IntRange(min=0, clamp=True)) @click.option( "-p", "--use-predefined-template", is_flag=True, default=False, - help="use predefined template", + help="Use a predefined template.", ) @click.option( "--redirect-stdout", is_flag=True, default=False, - help="Redirect stdout to /dev/null", + help="Redirect stdout to /dev/null.", ) @click.option( "--redirect-stderr", is_flag=True, default=False, - help="Redirect stderr to /dev/null", + help="Redirect stderr to /dev/null.", ) @click.option( - "--stream", is_flag=True, default=False, help="Input is a stream of JSON objects" + "--stream", + is_flag=True, + default=False, + help="Input is a stream of JSON objects.", ) @click.option( "-e", "--editor", is_flag=True, default=False, - help="Open an editor before running commands to allow manual changes", + help="Open an editor before running commands to allow manual changes.", ) @click.option( "-n", "--dry-run", is_flag=True, default=False, - help="perform a trial run without actually executing the command", + help="Perform a trial run without executing commands.", ) @click.version_option() @click.argument("command") def targs( command: str, - read_from: click.File, + read_from: TextIO, + stream: bool, + interactive: bool, + editor: bool, + template_engine: TemplateExtensions, + use_predefined_template: bool, shell: bool, - template_engine: str, - max_procs: int, redirect_stdout: bool, redirect_stderr: bool, - use_predefined_template: bool, - stream: bool, - editor: bool, - verbose: bool, + max_procs: int, dry_run: bool, + verbose: bool, ) -> None: """Execute commands based on a template string. @@ -93,41 +123,44 @@ def targs( --stream option is used), which are then supplied to the templating engine, to generate the command to run. """ - + data: Iterable[dict[str, Any]] if stream: - data = map(utils.json_loads, read_from) # type: ignore + data = map(utils.json_loads, read_from) else: - data = utils.json_loads(read_from.read()) # type: ignore + data = utils.json_loads(read_from.read()) if use_predefined_template: try: command, template_engine = config.load_template(command, "targs") except FileNotFoundError: - click.secho(f"Command {command} not found!", fg="red", err=True) - raise SystemExit + click.secho(f"Command template '{command}' not found!", fg="red", err=True) + raise SystemExit(1) except RuntimeError: - click.secho("Multiple files found!", fg="red", err=True) - raise SystemExit - - try: - render = template.get_render_function(command, engine=template_engine) - except RuntimeError: - click.secho("jinja2 not installed!", fg="red", err=True) - raise SystemExit + click.secho( + f"Multiple template files found for '{command}'!", + fg="red", + err=True, + ) + raise SystemExit(1) + render = template.get_render_function( + command, + "targs", + engine=template_engine, + ) commands = [render(r) for r in data] if editor: try: if (new_commands := utils.edit(commands)) is not None: - commands = list(new_commands.values()) - + commands = new_commands except RuntimeError: click.secho("Your edit is not parseable!", fg="red", err=True) - raise SystemExit + raise SystemExit(1) stdout = subprocess.DEVNULL if redirect_stdout else None stderr = subprocess.DEVNULL if redirect_stderr else None + patched_subprocess = functools.partial( subprocess.run, shell=shell, @@ -135,27 +168,20 @@ def targs( stderr=stderr, ) - if not dry_run: - patched_run_command = functools.partial( - run_command, - run=patched_subprocess, - shell=shell, - ) - else: - patched_run_command = functools.partial( - run_command, - run=print, - shell=True, - ) + execute_command = functools.partial( + run_command, + run=print if dry_run else patched_subprocess, + shell=True if dry_run else shell, + ) max_procs = utils.max_procs() if max_procs == 0 else max_procs if max_procs == 1: - for r in commands: - patched_run_command(r) + for cmd in commands: + execute_command(cmd) else: - with multiprocessing.Pool(max_procs) as p: - p.map(patched_run_command, commands) + with multiprocessing.Pool(max_procs) as pool: + pool.map(execute_command, commands) if __name__ == "__main__": diff --git a/patternutils/commands/tln.py b/patternutils/commands/tln.py index 878f556..cd2d4fe 100644 --- a/patternutils/commands/tln.py +++ b/patternutils/commands/tln.py @@ -1,20 +1,22 @@ -import json import os import os.path +from collections.abc import Iterable +from typing import Any, TextIO import click from .. import config, template, utils +from ..types import TemplateExtensions -def link_file(old_filename: str, new_filename: str, symbolic: bool) -> None: - if dirs := os.path.dirname(new_filename): +def link_file(source: str, destination: str, symbolic: bool) -> None: + if dirs := os.path.dirname(destination): os.makedirs(dirs, exist_ok=True) if symbolic: - os.symlink(old_filename, new_filename) + os.symlink(source, destination) else: - os.link(old_filename, new_filename) + os.link(source, destination) @click.command(context_settings={"help_option_names": ("-h", "--help", "-?")}) @@ -54,10 +56,16 @@ def link_file(old_filename: str, new_filename: str, symbolic: bool) -> None: help="select the template engine to use", ) @click.option( - "-k", "--key", default="_abspath", help="select the key to use as the source file" + "-k", + "--key", + default="_abspath", + help="select the key to use as the source file", ) @click.option( - "--stream", is_flag=True, default=False, help="Input is a stream of JSON objects" + "--stream", + is_flag=True, + default=False, + help="Input is a stream of JSON objects", ) @click.option( "-p", @@ -66,7 +74,11 @@ def link_file(old_filename: str, new_filename: str, symbolic: bool) -> None: default=False, help="use predefined template", ) -@click.option("--read-from", default="-", type=click.File("r")) +@click.option( + "--read-from", + default="-", + type=click.File("r"), +) @click.option( "--abort-on-path-exist", is_flag=True, @@ -74,6 +86,12 @@ def link_file(old_filename: str, new_filename: str, symbolic: bool) -> None: help="exit if a renamed path already exists", ) @click.option( + "--abort-on-key-not-found", + is_flag=True, + default=False, + help="Exit if the specified key (specified by the --key option) is not found in the input JSON.", +) +@click.option( "-n", "--dry-run", is_flag=True, @@ -81,7 +99,11 @@ def link_file(old_filename: str, new_filename: str, symbolic: bool) -> None: help="perform a trial run with no changes made", ) @click.option( - "-v", "--verbose", is_flag=True, default=False, help="explain what is being done" + "-v", + "--verbose", + is_flag=True, + default=False, + help="explain what is being done", ) @click.version_option() @click.argument("output-pattern") @@ -91,20 +113,21 @@ def tln( editor: bool, force: bool, symbolic: bool, - template_engine: str, + template_engine: TemplateExtensions, use_predefined_template: bool, - read_from: click.File, + read_from: TextIO, key: str, stream: bool, abort_on_path_exist: bool, + abort_on_key_not_found: bool, dry_run: bool, verbose: bool, ) -> None: - + data: Iterable[dict[str, Any]] if stream: - data = map(utils.json_loads, read_from) # type: ignore + data = map(utils.json_loads, read_from) else: - data = utils.json_loads(read_from.read()) # type: ignore + data = utils.json_loads(read_from.read()) if use_predefined_template: try: @@ -118,13 +141,19 @@ def tln( click.secho("Multiple files found!", fg="red", err=True) raise SystemExit + render = template.get_render_function( + output_pattern, + "tln", + engine=template_engine, + ) + new_filenames = dict() try: - render = template.get_render_function(output_pattern, engine=template_engine) - except RuntimeError: - click.secho("jinja2 not installed!", fg="red", err=True) - raise SystemExit - - new_filenames = dict((entry["_abspath"], render(**entry)) for entry in data) + for entry in data: + new_filenames[entry[key]] = render(**entry) + except KeyError: + click.secho(f"Key '{key}' not found in input data.", fg="red", err=True) + if abort_on_key_not_found: + raise SystemExit(1) if editor: try: @@ -146,12 +175,12 @@ def tln( if has_existing and abort_on_path_exist: raise SystemExit - files_to_move = new_filenames.items() if force else non_existing.items() + files_to_link = new_filenames.items() if force else non_existing.items() - for old, new in files_to_move: + for src, dst in files_to_link: if verbose or interactive: - click.echo(f"{old} -> {new}") + click.echo(f"{src} -> {dst}") if not (interactive and not click.confirm("Is the match correct?")): if not dry_run: - link_file(old, new, symbolic) + link_file(src, dst, symbolic) diff --git a/patternutils/commands/tmv.py b/patternutils/commands/tmv.py index fa7105e..58a75e5 100644 --- a/patternutils/commands/tmv.py +++ b/patternutils/commands/tmv.py @@ -1,14 +1,16 @@ -import json import os.path import shutil +from collections.abc import Iterable +from typing import Any, TextIO import click from .. import config, template, utils +from ..types import TemplateExtensions def move_file(old_filename: str, new_filename: str) -> None: - # Create new directories if needed + """Moves a file to a new location, creating necessary directories if needed.""" dirs = os.path.dirname(new_filename) if dirs: os.makedirs(dirs, exist_ok=True) @@ -22,56 +24,80 @@ def move_file(old_filename: str, new_filename: str) -> None: "--interactive", is_flag=True, default=False, - help="prompt before renaming files", + help="Prompt before renaming files.", ) @click.option( "-e", "--editor", is_flag=True, default=False, - help="Open an editor before renaming files to allow manual changes", + help="Open an editor before renaming files to allow manual changes.", +) +@click.option( + "-f", + "--force", + is_flag=True, + default=False, + help="Force rename, even if a file exists.", ) -@click.option("-f", "--force", is_flag=True, default=False, help="force rename") @click.option( "-t", "--template-engine", default="python", type=click.Choice(["python", "jinja2"]), - help="select the template engine to use", + help="Select the template engine to use.", ) @click.option( - "-k", "--key", default="_abspath", help="select the key to use as the source file" + "-k", + "--key", + default="_abspath", + help="Select the key to use as the source file.", ) @click.option( "-s", "--stream", is_flag=True, default=False, - help="Input is a stream of JSON objects", + help="Read input as stream of JSON objects.", ) @click.option( "-p", "--use-predefined-template", is_flag=True, default=False, - help="use predefined template", + help="Use a predefined template.", +) +@click.option( + "--read-from", + default="-", + type=click.File("r"), + help="Input file (default: stdin)", ) -@click.option("--read-from", default="-", type=click.File("r")) @click.option( "--abort-on-path-exist", is_flag=True, default=False, - help="exit if a renamed path already exists", + help="Exit if a renamed path already exists.", +) +@click.option( + "--abort-on-key-not-found", + is_flag=True, + default=False, + help="Exit if the specified key (specified by the --key option) is not found in the input JSON.", ) @click.option( "-n", "--dry-run", is_flag=True, default=False, - help="perform a trial run with no changes made", + help="Perform a trial run without making changes.", ) @click.option( - "-v", "--verbose", is_flag=True, default=False, help="explain what is being done" + "-v", + "--verbose", + is_flag=True, + default=False, + help="Explain what is being done.", ) @click.version_option() @click.argument("output-pattern") @@ -80,29 +106,30 @@ def tmv( interactive: bool, editor: bool, force: bool, - template_engine: str, + template_engine: TemplateExtensions, abort_on_path_exist: bool, + abort_on_key_not_found: bool, use_predefined_template: bool, - read_from: click.File, + read_from: TextIO, key: str, stream: bool, dry_run: bool, verbose: bool, ) -> None: - """Rename files based on the OUTPUT_PATTERN. - It reads in a JSON list containing objects or a stream of objects (if the - --stream option is used). The object needs to contain a key, named as - the specified option KEY (default: _abspath), which points to the file to - be moved. The rest of the keys will be supplied to the templating engine to - generate the new filename. + Reads a JSON list containing objects or a stream of JSON objects (if + --stream is used). The object must contain a key (specified via --key, + default: _abspath) that points to the file to be renamed. The rest of the + keys will be supplied to the templating engine to generate the new + filename. """ + data: Iterable[dict[str, Any]] if stream: - data = map(utils.json_loads, read_from) # type: ignore + data = map(utils.json_loads, read_from) else: - data = utils.json_loads(read_from.read()) # type: ignore + data = utils.json_loads(read_from.read()) if use_predefined_template: try: @@ -110,19 +137,29 @@ def tmv( output_pattern, "tmv" ) except FileNotFoundError: - click.secho(f"Template {output_pattern} not found!", fg="red", err=True) - raise SystemExit + click.secho(f"Template '{output_pattern}' not found!", fg="red", err=True) + raise SystemExit(1) except RuntimeError: - click.secho("Multiple files found!", fg="red", err=True) - raise SystemExit + click.secho( + f"Multiple template files found for '{output_pattern}'!", + fg="red", + err=True, + ) + raise SystemExit(1) + render = template.get_render_function( + output_pattern, + "tmv", + engine=template_engine, + ) + new_filenames = dict() try: - render = template.get_render_function(output_pattern, engine=template_engine) - except RuntimeError: - click.secho("jinja2 not installed!", fg="red", err=True) - raise SystemExit - - new_filenames = dict((entry[key], render(**entry)) for entry in data) + for entry in data: + new_filenames[entry[key]] = render(**entry) + except KeyError: + click.secho(f"Key '{key}' not found in input data.", fg="red", err=True) + if abort_on_key_not_found: + raise SystemExit(1) if editor: try: @@ -130,19 +167,19 @@ def tmv( new_filenames = temporary_filenames except RuntimeError: click.secho("Your edit is not parseable!", fg="red", err=True) - raise SystemExit + raise SystemExit(1) has_existing = False non_existing = {} for old, new in new_filenames.items(): if os.path.exists(new): - click.secho(f"{new} does exist!", fg="red", err=True) + click.secho(f"File '{new}' already exists!", fg="red", err=True) has_existing = True else: non_existing[old] = new if has_existing and abort_on_path_exist: - raise SystemExit + raise SystemExit(1) files_to_move = new_filenames.items() if force else non_existing.items() diff --git a/patternutils/config.py b/patternutils/config.py index c444478..4d38b21 100644 --- a/patternutils/config.py +++ b/patternutils/config.py @@ -1,33 +1,125 @@ import glob import os.path -from typing import Optional +import re +from typing import Optional, cast, get_args, overload import click +from .types import ( + PredefinedCommands, + PredefinedExtensions, + RegexCommands, + RegexExtensions, + TemplateCommands, + TemplateExtensions, +) + CONFIG_PATH = click.get_app_dir("patternutils") -def load_template(template_name: str, command_name: Optional[str]) -> tuple[str, str]: - """Load a template from the config directory.""" +@overload +def load_template( + template_name: str, command_name: RegexCommands +) -> tuple[str, RegexExtensions]: ... + + +@overload +def load_template( + template_name: str, command_name: TemplateCommands +) -> tuple[str, TemplateExtensions]: ... + + +def load_template( + template_name: str, command_name: Optional[PredefinedCommands] +) -> tuple[str, PredefinedExtensions]: + """Load a template from the config directory. + + Args: + template_name: The name of the template file (without extension). + command_name: The command requesting the template. + + Returns: + A tuple containing the template content as a string and its engine type. + + Raises: + FileNotFoundError: If no matching template is found. + RuntimeError: If multiple templates are found for the given name. + ValueError: If the extracted template extension is invalid. + """ files = find_templates(template_name, command_name) - if len(files) == 1: - template_file = files[0] - with open(template_file, encoding="utf-8") as f: - template_lines = f.read() - elif not files: - raise FileNotFoundError - elif len(files) > 1: - raise RuntimeError("Multiple files found!") + + if not files: + raise FileNotFoundError(f"No template found for '{template_name}'.") + + if len(files) > 1: + raise RuntimeError(f"Multiple template files found for '{template_name}'.") + + template_file = files[0] + with open(template_file, encoding="utf-8") as f: + template_lines = f.read() output_pattern = template_lines.replace("\n", "") - template_engine = os.path.splitext(template_file)[1][1:] + extension = os.path.splitext(template_file)[1][1:].lower() + + if extension not in get_args(PredefinedExtensions): + raise ValueError( + f"Invalid template extension '{extension}' in '{template_file}'." + ) + template_engine = cast(TemplateExtensions, extension) return output_pattern, template_engine -def find_templates(template_name: str, command_name: Optional[str] = None) -> list[str]: - template_glob = f"{template_name}.*" - command_glob = "*" if command_name is None else command_name - command_glob = os.path.join(CONFIG_PATH, command_glob) - return glob.glob(os.path.join(command_glob, template_glob)) +def find_templates( + template_name: str, command_name: Optional[PredefinedCommands] = None +) -> list[str]: + """Finds matching template files based on the template name and command. + + Args: + template_name: The name of the template file (without extension). + command_name: The command requesting the template. + + Returns: + A list of matching template file paths. + + Raises: + ValueError: If `command_name` is invalid. + """ + + if command_name is None: + # CONFIG_PATH/*/TEMPLATE_NAME.* + path_sub_regex = ".*" + extension = ".*" + else: + path_sub_regex = f"(globals|{command_name})" + + if command_name in get_args(RegexCommands): + extensions = get_args(RegexExtensions) + elif command_name in get_args(TemplateCommands): + # CONFIG_PATH/globals/TEMPLATE_NAME.jinja2 + # CONFIG_PATH/globals/TEMPLATE_NAME.python + # CONFIG_PATH/COMMAND_NAME/TEMPLATE_NAME.jinja2 + # CONFIG_PATH/COMMAND_NAME/TEMPLATE_NAME.python + extensions = get_args(TemplateExtensions) + else: + raise ValueError(f"Unknown command '{command_name}'.") + + if len(extensions) > 0: + if len(extensions) > 1: + extension = f"({'|'.join(extensions)})" + else: + extension = extensions[0] + else: + raise ValueError( + f"Could not determine file extension for command '{command_name}'." + ) + + pattern = re.compile( + os.path.join(CONFIG_PATH, path_sub_regex, f"{template_name}.{extension}") + ) + return [ + file + for file in glob.iglob(os.path.join(CONFIG_PATH, "*", "*")) + if pattern.search(file) + ] diff --git a/patternutils/match.py b/patternutils/match.py index fc6a805..9ff49f9 100644 --- a/patternutils/match.py +++ b/patternutils/match.py @@ -2,9 +2,21 @@ from typing import Pattern def apply(subject: str, regex_pattern: Pattern[str]) -> dict[str, str]: + """Applies a regex pattern to a subject string and extracts named groups. + + Args: + subject: The string to be matched. + regex_pattern: A compiled regex pattern. + + Returns: + A dictionary containing matched named groups, including `_subject`. + + Raises: + ValueError: If the subject does not match the regex pattern. + """ if (match := regex_pattern.search(subject)) is not None: groupdict = match.groupdict() groupdict["_subject"] = subject return groupdict else: - raise ValueError("subject doesn't match regex pattern") + raise ValueError(f"Subject does not match the regex pattern: {subject}") diff --git a/patternutils/template.py b/patternutils/template.py index f0b3c43..19de66c 100644 --- a/patternutils/template.py +++ b/patternutils/template.py @@ -1,54 +1,88 @@ +import os.path +import shlex +from datetime import datetime from typing import Callable -try: - import jinja2 -except ImportError: - jinja_feature = False -else: - import os.path - import shlex - from datetime import datetime +import jinja2 - jinja_feature = True - jinja_env = jinja2.Environment() +from .config import CONFIG_PATH +from .types import TemplateCommands, TemplateExtensions - def to_datetime(value: str, format: str = "%Y-%m-%d") -> datetime: - return datetime.strptime(value, format) +JINJA_ENV = jinja2.Environment() - def format_datetime(value: datetime, format: str = "%Y-%m-%d") -> str: - return value.strftime(format) - def to_int(value: str, base: int = 10) -> int: - return int(value, base=base) +def to_datetime(value: str, format: str = "%Y-%m-%d") -> datetime: + """Converts a string to a datetime object.""" + return datetime.strptime(value, format) - def to_float(value: str) -> float: - return float(value) - def quote(value: str) -> str: - return shlex.quote(value) +def format_datetime(value: datetime, format: str = "%Y-%m-%d") -> str: + """Formats a datetime object as a string.""" + return value.strftime(format) - def splitpath(value: str) -> tuple[str, str]: - return os.path.split(value) - def splitext(value: str) -> tuple[str, str]: - return os.path.splitext(value) +def to_int(value: str, base: int = 10) -> int: + """Converts a string to an integer.""" + return int(value, base=base) - jinja_env.filters["datetime"] = to_datetime - jinja_env.filters["datetimeformat"] = format_datetime - jinja_env.filters["int"] = to_int - jinja_env.filters["float"] = to_float - jinja_env.filters["quote"] = quote - jinja_env.filters["splitpath"] = splitpath - jinja_env.filters["splitext"] = splitext +def to_float(value: str) -> float: + """Converts a string to a float.""" + return float(value) -def get_render_function(template: str, *, engine: str = "python") -> Callable[..., str]: + +def quote(value: str) -> str: + """Quotes a string safely for shell execution.""" + return shlex.quote(value) + + +def splitpath(value: str) -> tuple[str, str]: + """Splits a path into (directory, filename).""" + return os.path.split(value) + + +def splitext(value: str) -> tuple[str, str]: + """Splits a file path into (root, extension).""" + return os.path.splitext(value) + + +JINJA_ENV.filters["datetime"] = to_datetime +JINJA_ENV.filters["datetimeformat"] = format_datetime +JINJA_ENV.filters["int"] = to_int +JINJA_ENV.filters["float"] = to_float +JINJA_ENV.filters["quote"] = quote +JINJA_ENV.filters["splitpath"] = splitpath +JINJA_ENV.filters["splitext"] = splitext + + +def get_render_function( + template: str, + command_name: TemplateCommands, + *, + engine: TemplateExtensions = "python", +) -> Callable[..., str]: + """Returns a function that renders a template using the specified engine. + + Args: + template: The template string. + engine: The template engine to use ('python' or 'jinja2'). + + Returns: + A function that takes parameters and renders the template. + + Raises: + RuntimeError: If Jinja2 is selected but not installed. + """ if engine == "jinja2": - if jinja_feature: - env_template = jinja_env.from_string(template) - render_function = env_template.render - else: - raise RuntimeError("jinja2 not installed!") + jinja_env_overlay = JINJA_ENV.overlay( + loader=jinja2.ChoiceLoader( + [ + jinja2.FileSystemLoader(os.path.join(CONFIG_PATH, command_name)), + jinja2.FileSystemLoader(os.path.join(CONFIG_PATH, "globals")), + ] + ) + ) + env_template = jinja_env_overlay.from_string(template) + return env_template.render else: - render_function = template.format - return render_function + return template.format diff --git a/patternutils/types.py b/patternutils/types.py new file mode 100644 index 0000000..ad27ebf --- /dev/null +++ b/patternutils/types.py @@ -0,0 +1,14 @@ +from typing import Literal + +PlainCommands = Literal["pmediainfo"] + +RegexCommands = Literal["pmatch", "pjson"] +RegexExtensions = Literal["regex"] + +TemplateCommands = Literal["tmv", "targs", "tln"] +TemplateExtensions = Literal["python", "jinja2"] + +PredefinedCommands = Literal[RegexCommands, TemplateCommands] +PredefinedExtensions = Literal[TemplateExtensions, RegexExtensions] + +CommandNames = Literal[PlainCommands, TemplateCommands, RegexCommands] diff --git a/patternutils/utils.py b/patternutils/utils.py index 0395110..22d4123 100644 --- a/patternutils/utils.py +++ b/patternutils/utils.py @@ -1,67 +1,123 @@ import json import os -from typing import Any, Generator, Optional, Union +from typing import Any, Generator, Optional, overload import click def max_procs() -> int: + """Returns the number of CPU cores available, or 1 if unavailable.""" cpu_count = os.cpu_count() return 1 if cpu_count is None else cpu_count * 2 -def edit(data: Union[dict[str, str], list[str]]) -> Optional[dict[str, str]]: +@overload +def edit(data: dict[str, str]) -> Optional[dict[str, str]]: ... - longest = len(max(data, key=len)) + +@overload +def edit(data: list[str]) -> Optional[list[str]]: ... + + +def edit(data: dict[str, str] | list[str]) -> Optional[dict[str, str] | list[str]]: + """Opens an interactive editor for modifying dictionary or list entries. + + Args: + data: A dictionary (keys are editable) or a list of strings. + + Returns: + A dictionary of modified entries or None if no changes were made. + """ if isinstance(data, dict): - temporary = [f"{key.ljust(longest)}\t{value}\n" for key, value in data.items()] + longest = len(max(data, key=len)) + temporary = [f"{key.ljust(longest)}\t{value}" for key, value in data.items()] else: - temporary = [f"{value.ljust(longest)}\t{value}\n" for value in data] + temporary = data try: - if (edited := click.edit("".join(temporary))) is not None: - new_data = dict() - for line in edited.split("\n"): - if line and "\t" in line: - old, new = line.split("\t") - new_data[old.strip()] = new - if new_data: - return new_data + if (edited := click.edit("\n".join(temporary))) is not None: + if isinstance(data, dict): + new_data = dict() + for line in edited.split("\n"): + if line and "\t" in line: + old, new = line.split("\t") + new_data[old.strip()] = new + + if new_data: + return new_data + else: + raise RuntimeError else: - raise RuntimeError + return edited.split("\n") else: return None except click.UsageError: click.secho("The editor could not be launced!", fg="red", err=True) - raise SystemExit + raise SystemExit(1) def json_dumps(data: Any, human_readable: bool = False) -> str: - config = ( - {"indent": 4, "sort_keys": True} - if human_readable - else {"separators": (",", ":")} - ) # type: dict[str, Any] - return json.dumps(data, **config) + """Serializes data to JSON with optional human-readable formatting. + + Args: + data: The data to serialize. + human_readable: Whether to pretty-print the JSON. + + Returns: + A JSON string. + """ + return json.dumps( + data, + indent=4 if human_readable else None, + sort_keys=human_readable, + separators=(", ", ": ") if not human_readable else None, + ) def json_loads(data: Any) -> Any: + """Deserializes a JSON string into a Python object. + + Args: + data: A JSON string. + + Returns: + The deserialized Python object. + + Raises: + SystemExit: If the JSON is invalid. + """ try: return json.loads(data) - except json.JSONDecodeError: - click.secho("JSON not valid!", fg="red", err=True) - raise SystemExit + except json.JSONDecodeError as e: + click.secho(f"Invalid JSON: {e}!", fg="red", err=True) + raise SystemExit(1) def walk( directory: str, recursive: bool = False, match_directories: bool = False -) -> Generator[os.DirEntry[Any], None, None]: - for entry in os.scandir(directory): - is_dir = entry.is_dir() - if recursive and is_dir: - yield from walk(entry.path, recursive, match_directories) - elif entry.is_file() or (match_directories and is_dir): - yield entry - else: - continue +) -> Generator[os.DirEntry[str], None, None]: + """Yields directory entries, optionally recursing into subdirectories. + + Args: + directory: The root directory to scan. + recursive: Whether to recurse into subdirectories. + match_directories: Whether to include directories in the results. + + Yields: + os.DirEntry objects matching the criteria. + """ + try: + for entry in os.scandir(directory): + if entry.is_dir(): + if recursive: + yield from walk(entry.path, recursive, match_directories) + if match_directories: + yield entry + elif entry.is_file(): + yield entry + else: + continue + except FileNotFoundError: + click.secho(f"Directory not found: {directory}", fg="red", err=True) + raise SystemExit(1) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 486a87d..0000000 --- a/poetry.lock +++ /dev/null @@ -1,335 +0,0 @@ -[[package]] -name = "black" -version = "21.12b0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -click = ">=7.1.2" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" -platformdirs = ">=2" -tomli = ">=0.2.6,<2.0.0" -typing-extensions = [ - {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, - {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, -] - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.3)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "click" -version = "8.0.3" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.4" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "feedparser" -version = "6.0.8" -description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -sgmllib3k = "*" - -[[package]] -name = "isort" -version = "5.10.1" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.6.1,<4.0" - -[package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] -plugins = ["setuptools"] - -[[package]] -name = "jinja2" -version = "3.0.3" -description = "A very fast and expressive template engine." -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markupsafe" -version = "2.0.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = true -python-versions = ">=3.6" - -[[package]] -name = "mypy" -version = "0.930" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = ">=1.1.0" -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "pathspec" -version = "0.9.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "platformdirs" -version = "2.4.1" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] - -[[package]] -name = "pygments" -version = "2.11.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "main" -optional = true -python-versions = ">=3.5" - -[[package]] -name = "sgmllib3k" -version = "1.0.0" -description = "Py3k port of sgmllib." -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "tomli" -version = "1.2.3" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "vermin" -version = "1.3.3" -description = "Concurrently detect the minimum Python versions needed to run code" -category = "dev" -optional = false -python-versions = ">=2.7" - -[extras] -jinja2 = ["Jinja2"] -prss = ["feedparser"] -pygments = ["Pygments"] - -[metadata] -lock-version = "1.1" -python-versions = ">=3.8,<4" -content-hash = "eedebf0b6e78c4e1fa28738ea11b5cb637b36821c647bdbc53456ab1cf477790" - -[metadata.files] -black = [ - {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, - {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, -] -click = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -feedparser = [ - {file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"}, - {file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, -] -markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, -] -mypy = [ - {file = "mypy-0.930-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:221cc94dc6a801ccc2be7c0c9fd791c5e08d1fa2c5e1c12dec4eab15b2469871"}, - {file = "mypy-0.930-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db3a87376a1380f396d465bed462e76ea89f838f4c5e967d68ff6ee34b785c31"}, - {file = "mypy-0.930-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d2296f35aae9802eeb1327058b550371ee382d71374b3e7d2804035ef0b830b"}, - {file = "mypy-0.930-cp310-cp310-win_amd64.whl", hash = "sha256:959319b9a3cafc33a8185f440a433ba520239c72e733bf91f9efd67b0a8e9b30"}, - {file = "mypy-0.930-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:45a4dc21c789cfd09b8ccafe114d6de66f0b341ad761338de717192f19397a8c"}, - {file = "mypy-0.930-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1e689e92cdebd87607a041585f1dc7339aa2e8a9f9bad9ba7e6ece619431b20c"}, - {file = "mypy-0.930-cp36-cp36m-win_amd64.whl", hash = "sha256:ed4e0ea066bb12f56b2812a15ff223c57c0a44eca817ceb96b214bb055c7051f"}, - {file = "mypy-0.930-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a9d8dffefba634b27d650e0de2564379a1a367e2e08d6617d8f89261a3bf63b2"}, - {file = "mypy-0.930-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b419e9721260161e70d054a15abbd50603c16f159860cfd0daeab647d828fc29"}, - {file = "mypy-0.930-cp37-cp37m-win_amd64.whl", hash = "sha256:601f46593f627f8a9b944f74fd387c9b5f4266b39abad77471947069c2fc7651"}, - {file = "mypy-0.930-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ea7199780c1d7940b82dbc0a4e37722b4e3851264dbba81e01abecc9052d8a7"}, - {file = "mypy-0.930-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:70b197dd8c78fc5d2daf84bd093e8466a2b2e007eedaa85e792e513a820adbf7"}, - {file = "mypy-0.930-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5feb56f8bb280468fe5fc8e6f56f48f99aa0df9eed3c507a11505ee4657b5380"}, - {file = "mypy-0.930-cp38-cp38-win_amd64.whl", hash = "sha256:2e9c5409e9cb81049bb03fa1009b573dea87976713e3898561567a86c4eaee01"}, - {file = "mypy-0.930-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:554873e45c1ca20f31ddf873deb67fa5d2e87b76b97db50669f0468ccded8fae"}, - {file = "mypy-0.930-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0feb82e9fa849affca7edd24713dbe809dce780ced9f3feca5ed3d80e40b777f"}, - {file = "mypy-0.930-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc1a0607ea03c30225347334af66b0af12eefba018a89a88c209e02b7065ea95"}, - {file = "mypy-0.930-cp39-cp39-win_amd64.whl", hash = "sha256:f9f665d69034b1fcfdbcd4197480d26298bbfb5d2dfe206245b6498addb34999"}, - {file = "mypy-0.930-py3-none-any.whl", hash = "sha256:bf4a44e03040206f7c058d1f5ba02ef2d1820720c88bc4285c7d9a4269f54173"}, - {file = "mypy-0.930.tar.gz", hash = "sha256:51426262ae4714cc7dd5439814676e0992b55bcc0f6514eccb4cf8e0678962c2"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -platformdirs = [ - {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, - {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, -] -pygments = [ - {file = "Pygments-2.11.0-py3-none-any.whl", hash = "sha256:ac8098bfc40b8e1091ad7c13490c7f4797e401d0972e8fcfadde90ffb3ed4ea9"}, - {file = "Pygments-2.11.0.tar.gz", hash = "sha256:51130f778a028f2d19c143fce00ced6f8b10f726e17599d7e91b290f6cbcda0c"}, -] -sgmllib3k = [ - {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, -] -tomli = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, -] -typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, -] -vermin = [ - {file = "vermin-1.3.3-py2.py3-none-any.whl", hash = "sha256:c5bd8bf2026d87563332293a0479606954bd973d8cf72ab370e1164a019c0524"}, -] diff --git a/pyproject.toml b/pyproject.toml index 4a340da..da0d5f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,32 @@ -[tool.poetry] +[project] name = "patternutils" -version = "0.0.12" description = "Combine regular expressions with template engines in a powerful matter." -authors = ["Dennis Fink <dennis.fink@c3l.lu>"] -license = "GPL-3.0-or-later" +version = "0.0.13" +authors = [ + {name = "Dennis Fink", email = "dennis.fink@c3l.lu"}, +] +license = {text = "GPL-3.0-or-later"} +requires-python = ">=3.12, <4" +dependencies = [ + "click >=8.1.8, <9.0.0", + "Jinja2 >=3.1.5, <4.0.0", +] + +[project.urls] repository = "https://projects.metalgamer.eu/patternutils" -[tool.poetry.scripts] +[project.optional-dependencies] +pygments = [ + "Pygments >=2.19.1, <3.0.0", +] +prss = [ + "feedparser >=6.0.11, <7.0.0", +] +pmediainfo = [ + "pymediainfo >=7.0.1, <8.0.0", +] + +[project.scripts] pmatch = "patternutils.commands.pmatch:pmatch" targs = "patternutils.commands.targs:targs" tmv = "patternutils.commands.tmv:tmv" @@ -14,25 +34,23 @@ tln = "patternutils.commands.tln:tln" patterncfg = "patternutils.commands.patterncfg:patterncfg" prss = "patternutils.commands.prss:prss" pjson = "patternutils.commands.pjson:pjson" +pmediainfo = "patternutils.commands.pmediainfo:pmediainfo" -[tool.poetry.dependencies] -python = ">=3.8,<4" -click = "^8.0.3" -Jinja2 = {version = "^3.0.3", optional = true} -Pygments = {version = "^2.10.0", optional = true} -feedparser = {version = "^6.0.8", optional = true} +[dependency-groups] +dev = [ + "mypy >=1.15.0, <2.0.0", + "black >=25.1.0, <26.0.0", + "isort >=6.0.1, <7.0.0", + "vermin >=1.6.0, <2.0.0", +] -[tool.poetry.dev-dependencies] -mypy = "^0.930" -black = "^21.12b0" -isort = "^5.10.1" -vermin = "^1.3.2" +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" -[tool.poetry.extras] -jinja2 = ["Jinja2"] -pygments = ["Pygments"] -prss = ["feedparser"] -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.pdm.build] +includes = [] + +[tool.uv] +default-groups = [] @@ -0,0 +1,274 @@ +version = 1 +revision = 1 +requires-python = ">=3.12, <4" + +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "feedparser" +version = "6.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sgmllib3k" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/aa/7af346ebeb42a76bf108027fe7f3328bb4e57a3a96e53e21fd9ef9dd6dd0/feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5", size = 286197 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/d4/8c31aad9cc18f451c49f7f9cfb5799dadffc88177f7917bc90a66459b1d7/feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45", size = 81343 }, +] + +[[package]] +name = "isort" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186 }, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "patternutils" +version = "0.0.13" +source = { editable = "." } +dependencies = [ + { name = "click" }, +] + +[package.optional-dependencies] +jinja2 = [ + { name = "jinja2" }, +] +pmediainfo = [ + { name = "pymediainfo" }, +] +prss = [ + { name = "feedparser" }, +] +pygments = [ + { name = "pygments" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "isort" }, + { name = "mypy" }, + { name = "vermin" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.1.8,<9.0.0" }, + { name = "feedparser", marker = "extra == 'prss'", specifier = ">=6.0.11,<7.0.0" }, + { name = "jinja2", marker = "extra == 'jinja2'", specifier = ">=3.1.5,<4.0.0" }, + { name = "pygments", marker = "extra == 'pygments'", specifier = ">=2.19.1,<3.0.0" }, + { name = "pymediainfo", marker = "extra == 'pmediainfo'", specifier = ">=7.0.1,<8.0.0" }, +] +provides-extras = ["jinja2", "pygments", "prss", "pmediainfo"] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=25.1.0,<26.0.0" }, + { name = "isort", specifier = ">=6.0.1,<7.0.0" }, + { name = "mypy", specifier = ">=1.15.0,<2.0.0" }, + { name = "vermin", specifier = ">=1.6.0,<2.0.0" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pymediainfo" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/80/80a6fb21005b81e30f6193d45cba13857df09f5d483e0551fa6fbb3aaeed/pymediainfo-7.0.1.tar.gz", hash = "sha256:0d5df59ecc615e24c56f303b8f651579c6accab7265715e5d429186d7ba21514", size = 441563 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/4a/d895646df3d3ff617b54d7f06a02ed9d6f5b86673030a543927310e0f7ed/pymediainfo-7.0.1-py3-none-macosx_10_10_universal2.whl", hash = "sha256:286f3bf6299be0997093254e0f371855bc5cf2aaf8641d19455a011e3ee3a84d", size = 6983332 }, + { url = "https://files.pythonhosted.org/packages/77/df/bc6b5a08e908c64a81f6ff169716d408ce7380ceff44e1eceb095f49e0dc/pymediainfo-7.0.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:3648e2379fa67bd02433d1e28c707df3a53834dd480680615a9fefd2266f1182", size = 5768082 }, + { url = "https://files.pythonhosted.org/packages/02/10/a9bc1446a48d3a15940eb1af79a71978f368f27e2cc86f9ec3ec2d206a20/pymediainfo-7.0.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:cde98112f1ce486589b17a12e5da42085faea996224f7c67fa45b8c1dca719c6", size = 6001553 }, + { url = "https://files.pythonhosted.org/packages/ed/7f/c48f8514cb60c9ff9be81b6f383e73e66c7461ef854a1b62628e3c823f13/pymediainfo-7.0.1-py3-none-win32.whl", hash = "sha256:01bcaf82b72cefbf4b96f13b2547e1b2e0e734bab7173d7c33f7f01acc07c98b", size = 3125046 }, + { url = "https://files.pythonhosted.org/packages/e7/26/9d50c2a330541bc36c0ea7ce29eeff5b0c35c2624139660df8bcfa9ae3ce/pymediainfo-7.0.1-py3-none-win_amd64.whl", hash = "sha256:13224fa7590e198763b8baf072e704ea81d334e71aa32a469091460e243893c7", size = 3271232 }, +] + +[[package]] +name = "sgmllib3k" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750 } + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "vermin" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/26/7b871396c33006c445c25ef7da605ecbd6cef830d577b496d2b73a554f9d/vermin-1.6.0.tar.gz", hash = "sha256:6266ca02f55d1c2aa189a610017c132eb2d1934f09e72a955b1eb3820ee6d4ef", size = 93181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/98/1a2ca43e6d646421eea16ec19977e2e6d1ea9079bd9d873bfae513d43f1c/vermin-1.6.0-py2.py3-none-any.whl", hash = "sha256:f1fa9ee40f59983dc40e0477eb2b1fa8061a3df4c3b2bcf349add462a5610efb", size = 90845 }, +] |
