summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.hgignore260
-rw-r--r--PKGBUILD/PKGBUILD19
-rw-r--r--patternutils/commands/patterncfg.py158
-rw-r--r--patternutils/commands/pjson.py13
-rw-r--r--patternutils/commands/pmatch.py94
-rw-r--r--patternutils/commands/pmediainfo.py100
-rw-r--r--patternutils/commands/prss.py10
-rw-r--r--patternutils/commands/targs.py138
-rw-r--r--patternutils/commands/tln.py77
-rw-r--r--patternutils/commands/tmv.py109
-rw-r--r--patternutils/config.py126
-rw-r--r--patternutils/match.py14
-rw-r--r--patternutils/template.py112
-rw-r--r--patternutils/types.py14
-rw-r--r--patternutils/utils.py122
-rw-r--r--poetry.lock335
-rw-r--r--pyproject.toml64
-rw-r--r--uv.lock274
18 files changed, 1342 insertions, 697 deletions
diff --git a/.hgignore b/.hgignore
index b22e97c..d96c129 100644
--- a/.hgignore
+++ b/.hgignore
@@ -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 = []
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..b170061
--- /dev/null
+++ b/uv.lock
@@ -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 },
+]