summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDennis Fink2022-02-26 00:59:03 +0100
committerDennis Fink2022-02-26 00:59:03 +0100
commitfe688d5d7f2fa67e6c114b01269c42690cb069b1 (patch)
treedc75e159a9d7f4856747a73387ae2ed2b2045368
parent70440dcbcdb4f016c10a0b2afd4c2c1fddd5d494 (diff)
parentc8694bdd3753c90da88338d9f3153192d5babc0a (diff)
downloadpatternutils-fe688d5d7f2fa67e6c114b01269c42690cb069b1.tar.gz
patternutils-fe688d5d7f2fa67e6c114b01269c42690cb069b1.zip
Merged default
-rw-r--r--.hgignore3
-rw-r--r--PKGBUILD/PKGBUILD15
-rw-r--r--patternutils/commands/patterncfg.py15
-rw-r--r--patternutils/commands/pjson.py131
-rw-r--r--patternutils/commands/pmatch.py62
-rw-r--r--patternutils/commands/prss.py143
-rw-r--r--patternutils/commands/targs.py66
-rw-r--r--patternutils/commands/tln.py28
-rw-r--r--patternutils/commands/tmv.py30
-rw-r--r--patternutils/config.py4
-rw-r--r--patternutils/match.py7
-rw-r--r--patternutils/template.py10
-rw-r--r--patternutils/utils.py74
-rw-r--r--poetry.lock335
-rw-r--r--pyproject.toml38
-rw-r--r--requirements.txt4
-rw-r--r--setup.py53
17 files changed, 820 insertions, 198 deletions
diff --git a/.hgignore b/.hgignore
index fc3ed61..b22e97c 100644
--- a/.hgignore
+++ b/.hgignore
@@ -5,6 +5,7 @@ syntax: glob
*.egg-info
*.tar.xz
*.tar.xz.sig
+*.tar.zst
build/*
dist/*
@@ -13,3 +14,5 @@ PKGBUILD/src/*
PKGBUILD/*.zip
PKGBUILD/.git/*
PKGBUILD/.SRCINFO
+venv/*
+.mypy_cache/*
diff --git a/PKGBUILD/PKGBUILD b/PKGBUILD/PKGBUILD
index ca4fba9..42ca51b 100644
--- a/PKGBUILD/PKGBUILD
+++ b/PKGBUILD/PKGBUILD
@@ -1,27 +1,26 @@
# Maintainer: Dennis Fink <dennis.fink@c3l.lu>
pkgname=patternutils
-pkgver=0.0.8
-_pkghash=853b2c08d397
+pkgver=0.0.12
pkgrel=1
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')
-source=('patternutils-0.0.8.zip::https://projects.metalgamer.eu/patternutils/archive/853b2c08d397ad61442b8caf7d8cd4723a1dd04e.zip?subrepos=false')
-sha512sums=('dc29e9403e260a04cbe986567a37ecd17bf2b335dbe99c3b274d5568252e22464b7bb52fd2e9dba459ac83d8f79eb39a832641f224fe0911522d60f6c0069a44')
+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-$_pkghash/setup.py"
+ sed -i 's/==/>=/g' "$srcdir/$pkgname-$pkgver/setup.py"
}
build() {
- cd "$srcdir/$pkgname-$_pkghash"
+ cd "$srcdir/$pkgname-$pkgver"
python setup.py build
}
package() {
- cd "$srcdir/$pkgname-$_pkghash"
+ cd "$srcdir/$pkgname-$pkgver"
python setup.py install --root="$pkgdir" --optimize=1
}
diff --git a/patternutils/commands/patterncfg.py b/patternutils/commands/patterncfg.py
index f619196..6b2f6ec 100644
--- a/patternutils/commands/patterncfg.py
+++ b/patternutils/commands/patterncfg.py
@@ -6,9 +6,9 @@ from typing import Optional
import click
try:
- from pygments import highlight
- from pygments.formatters import TerminalFormatter
- from pygments.lexers import DjangoLexer, Python3Lexer
+ from pygments import highlight # type: ignore
+ from pygments.formatters import TerminalFormatter # type: ignore
+ from pygments.lexers import DjangoLexer, Python3Lexer # type: ignore
except ImportError:
has_pygments = False
else:
@@ -137,12 +137,19 @@ def view(name: str, command: str, color: bool) -> None:
click.secho("File not found!", fg="red", err=True)
raise SystemExit
else:
- if color:
+ 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(
+ "Pygments is not installed!. Cannot colorize output.",
+ fg="red",
+ err=True,
+ )
+ raise SystemExit
click.echo(text[:-1])
diff --git a/patternutils/commands/pjson.py b/patternutils/commands/pjson.py
new file mode 100644
index 0000000..8604971
--- /dev/null
+++ b/patternutils/commands/pjson.py
@@ -0,0 +1,131 @@
+import json
+import re
+from typing import Any, Pattern
+
+import click
+
+from .. import match as mat
+from .. import utils
+
+
+def apply_regex(
+ data: dict[str, str], key: str, regex_pattern: Pattern[str]
+) -> dict[str, str]:
+ try:
+ subject = data[key]
+ except KeyError:
+ click.secho(f"{key} can not be found in data!", fg="red", err=True)
+ raise
+
+ try:
+ match = mat.apply(subject, regex_pattern)
+ except ValueError:
+ click.secho(f"{subject} did not match", fg="red", err=True)
+ raise
+ else:
+ return match
+
+
+@click.command(context_settings={"help_option_names": ("-h", "--help", "-?")})
+@click.option("--read-from", default="-", type=click.File("r"))
+@click.option(
+ "--stream",
+ is_flag=True,
+ default=False,
+ help="Input is a stream of JSON objects and output will also be a stream of JSON objects",
+)
+@click.option(
+ "--abort-on-no-key",
+ is_flag=True,
+ default=False,
+ help="exit if a key does not exist",
+)
+@click.option(
+ "--abort-on-no-match",
+ is_flag=True,
+ default=False,
+ help="exit if a value does not match the REGEX_PATTERN",
+)
+@click.option(
+ "--abort-on-empty-data",
+ is_flag=True,
+ default=False,
+ help="exit if the data is empty",
+)
+@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()
+@click.argument("key")
+@click.argument("regex-pattern")
+def pjson(
+ key: str,
+ regex_pattern: str,
+ read_from: click.File,
+ stream: bool,
+ abort_on_no_key: bool,
+ abort_on_no_match: bool,
+ abort_on_empty_data: bool,
+ human_readable: bool,
+ verbose: bool,
+) -> None:
+
+ try:
+ regex_pattern_c = re.compile(regex_pattern)
+ except re.error as e:
+ if verbose:
+ click.secho("Regular expression could not be compiled!", fg="red", err=True)
+ click.secho(e.msg, fg="red", err=True)
+ raise SystemExit
+
+ if stream:
+ data = map(utils.json_loads, read_from) # type: ignore
+ else:
+ data = utils.json_loads(read_from.read()) # type: ignore
+
+ matches = []
+
+ has_no_key = False
+ 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)
+ if stream and abort_on_empty_data:
+ raise SystemExit
+ continue
+
+ try:
+ match = apply_regex(line, key, regex_pattern_c)
+ except KeyError:
+ has_no_key = True
+ if stream and abort_on_no_key:
+ raise SystemExit
+ except ValueError:
+ has_no_match = True
+ if stream and abort_on_no_match:
+ raise SystemExit
+ else:
+ line.update(match)
+ if stream:
+ click.echo(utils.json_dumps(line, human_readable))
+ else:
+ matches.append(line)
+
+ if not stream:
+ if (
+ (abort_on_no_key and has_no_key)
+ or (abort_on_no_match and has_no_match)
+ or (abort_on_empty_data and has_empty_data)
+ ):
+ raise SystemExit
+
+ click.echo(utils.json_dumps(matches, human_readable))
diff --git a/patternutils/commands/pmatch.py b/patternutils/commands/pmatch.py
index 6bbd349..3884a9d 100644
--- a/patternutils/commands/pmatch.py
+++ b/patternutils/commands/pmatch.py
@@ -1,10 +1,8 @@
import functools
import itertools
-import json
-import os
import os.path
import re
-from typing import Any, Dict, Generator, Iterator, List, Pattern, Tuple
+from typing import Any, Generator, Iterator, Optional, Pattern
import click
@@ -13,43 +11,26 @@ from .. import match as mat
from .. import utils
-def walk(
- dir: str, recursive: bool = False, match_directories: bool = False
-) -> Generator[os.DirEntry, None, None]:
- for entry in os.scandir(dir):
- 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
-
-
def apply_regex(
walk_function: Iterator[Any],
regex_pattern: Pattern[str],
match_full_path: bool,
-) -> Tuple[List[Dict[str, str]], bool]:
+) -> Generator[Optional[dict[str, str]], None, None]:
- matches = []
- has_invalid_matches = False
for f in walk_function:
- subject = f.path if match_full_path else f.name
+ subject = os.path.abspath(f.path) if match_full_path else f.name
try:
match = mat.apply(subject, regex_pattern)
except ValueError:
click.secho(f"{subject} did not match", fg="red", err=True)
- has_invalid_matches = 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)
- matches.append(match)
-
- return matches, has_invalid_matches
+ yield match
@click.command(context_settings={"help_option_names": ("-h", "--help", "-?")})
@@ -78,7 +59,7 @@ def apply_regex(
"--match-full-path",
is_flag=True,
default=False,
- help="match on full path instead of only filename",
+ help="match on full absolute path instead of only the filename",
)
@click.option(
"-p",
@@ -106,7 +87,7 @@ def apply_regex(
@click.argument("regex-pattern")
def pmatch(
regex_pattern: str,
- directory: List[str],
+ directory: list[str],
match_directories: bool,
recursive: bool,
abort_on_no_match: bool,
@@ -132,26 +113,31 @@ def pmatch(
except re.error as e:
if verbose:
click.secho("Regular expression could not be compiled!", fg="red", err=True)
- click.secho(e.msg, fg="red", err=True) # type: ignore
+ click.secho(e.msg, fg="red", err=True)
raise SystemExit
# Monkey patch walk function to include commandline options
walk_function = functools.partial(
- walk, recursive=recursive, match_directories=match_directories
+ utils.walk, recursive=recursive, match_directories=match_directories
)
new_walk = itertools.chain.from_iterable(map(walk_function, directory))
- matches, has_invalid_matches = apply_regex(
- new_walk, regex_pattern_c, match_full_path
- )
-
- if has_invalid_matches and abort_on_no_match:
- raise SystemExit
+ has_invalid_matches = False
+ matches = []
+ 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
+ else:
+ if stream:
+ click.echo(utils.json_dumps(match, human_readable))
+ else:
+ matches.append(match)
- if stream:
- for m in matches:
- click.echo(utils.json_dumps(m, human_readable))
- else:
+ if not stream:
+ if has_invalid_matches and abort_on_no_match:
+ raise SystemExit
click.echo(utils.json_dumps(matches, human_readable))
diff --git a/patternutils/commands/prss.py b/patternutils/commands/prss.py
new file mode 100644
index 0000000..1ec4675
--- /dev/null
+++ b/patternutils/commands/prss.py
@@ -0,0 +1,143 @@
+import functools
+import itertools
+import os
+import os.path
+from typing import Any, Generator, Iterator, Optional
+
+import click
+
+try:
+ import feedparser # type: ignore
+except ImportError:
+ click.secho("feedparser not installed!", fg="red", err=True)
+ raise SystemExit
+
+from .. import utils
+
+
+def match(
+ parsed_feed: feedparser.util.FeedParserDict,
+ walk_function: functools.partial[Generator[os.DirEntry[Any], None, None]],
+ directory: list[str],
+) -> Generator[Optional[dict[str, str]], None, None]:
+
+ direntries = []
+ filenames = []
+
+ for f in itertools.chain.from_iterable(map(walk_function, directory)):
+ direntries.append(f)
+ filenames.append(f.name)
+
+ for entry in parsed_feed.entries:
+ href = entry.enclosures[0]["href"]
+ splitted_href = href.split("/")
+ file_to_search = splitted_href[-1]
+ try:
+ file_index = filenames.index(file_to_search)
+ except ValueError:
+ click.secho(f"{file_to_search} not found!", fg="red", err=True)
+ yield None
+ else:
+ filename = filenames[file_index]
+ direntry = direntries[file_index]
+ entry_values = {
+ "title": entry.get("title", None),
+ "link": entry.get("link", None),
+ "description": entry.get("description", None),
+ "id": entry.get("id", None),
+ "enclosure_href": href,
+ "published": entry.get("published", None),
+ "subtitle": entry.get("subtitle", None),
+ "summary": entry.get("summary", None),
+ "author": entry.get("author", None),
+ "_name": filename,
+ "_path": direntry.path,
+ "_abspath": os.path.abspath(direntry.path),
+ "_realpath": os.path.realpath(direntry.path),
+ "_relpath": os.path.relpath(direntry.path),
+ "feed_title": parsed_feed.feed.get("title", None),
+ "feed_link": parsed_feed.feed.get("link", None),
+ "feed_description": parsed_feed.feed.get("description", None),
+ }
+ yield entry_values
+
+
+@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-no-file",
+ is_flag=True,
+ default=False,
+ help="exit if a file can not be found",
+)
+@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()
+@click.argument("url")
+def prss(
+ url: str,
+ directory: list[str],
+ recursive: bool,
+ abort_on_no_file: bool,
+ stream: bool,
+ human_readable: bool,
+ verbose: bool,
+) -> None:
+
+ try:
+ parsed_feed = feedparser.parse(url)
+ except Exception:
+ click.secho("Reading feed failed!", fg="red", err=True)
+ raise SystemExit
+ else:
+ if not parsed_feed.version:
+ click.secho("Unsupported feed!", fg="red", err=True)
+ raise SystemExit
+
+ # Mokey patch walk function to include commandline options
+ walk_function = functools.partial(
+ utils.walk, recursive=recursive, match_directories=False
+ )
+
+ matches = []
+ has_no_file = False
+ for m in match(parsed_feed, walk_function, directory):
+ if m is None:
+ has_no_file = True
+ if abort_on_no_file and stream:
+ raise SystemExit
+ else:
+ if stream:
+ click.echo(utils.json_dumps(m, human_readable))
+ else:
+ matches.append(m)
+
+ if not stream:
+ if abort_on_no_file and has_no_file:
+ raise SystemExit
+
+ click.echo(utils.json_dumps(matches, human_readable))
+
+
+if __name__ == "__main__":
+ prss()
diff --git a/patternutils/commands/targs.py b/patternutils/commands/targs.py
index 04c286d..34cf018 100644
--- a/patternutils/commands/targs.py
+++ b/patternutils/commands/targs.py
@@ -4,7 +4,7 @@ import multiprocessing
import os
import shlex
import subprocess
-from typing import Callable, Dict, List, Union
+from typing import Callable
import click
@@ -12,16 +12,11 @@ from .. import config, template, utils
def run_command(
- data: Dict[str, str],
+ command: str,
run: Callable[..., None],
shell: bool,
) -> None:
- if not shell:
- prepared_command = shlex.split(c) # type: Union[str, List[str]]
- else:
- prepared_command = c
-
- run(prepared_command)
+ run(shlex.split(command) if not shell else command)
@click.command(context_settings={"help_option_names": ("-h", "--help", "-?")})
@@ -69,6 +64,13 @@ def run_command(
default=False,
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",
+)
@click.version_option()
@click.argument("command")
def targs(
@@ -83,16 +85,19 @@ def targs(
stream: bool,
editor: bool,
verbose: bool,
+ dry_run: bool,
) -> None:
+ """Execute commands based on a template string.
- try:
- if not stream:
- data = json.load(read_from) # type: ignore
- else:
- data = [json.loads(line) for line in read_from]
- except json.JSONDecodeError:
- click.secho("JSON not valid!", fg="red", err=True)
- raise SystemExit
+ It reads in a JSON list containing objects, or a stream of objects (if the
+ --stream option is used), which are then supplied to the templating engine,
+ to generate the command to run.
+ """
+
+ if stream:
+ data = map(utils.json_loads, read_from) # type: ignore
+ else:
+ data = utils.json_loads(read_from.read()) # type: ignore
if use_predefined_template:
try:
@@ -114,13 +119,13 @@ def targs(
if editor:
try:
- commands = utils.edit(commands)
- except RuntimeErro:
+ if (new_commands := utils.edit(commands)) is not None:
+ commands = list(new_commands.values())
+
+ except RuntimeError:
click.secho("Your edit is not parseable!", fg="red", err=True)
raise SystemExit
- max_procs = utils.max_procs() if max_procs == 0 else max_procs
-
stdout = subprocess.DEVNULL if redirect_stdout else None
stderr = subprocess.DEVNULL if redirect_stderr else None
patched_subprocess = functools.partial(
@@ -129,11 +134,22 @@ def targs(
stdout=stdout,
stderr=stderr,
)
- patched_run_command = functools.partial(
- run_command,
- run=patched_subprocess,
- shell=shell,
- )
+
+ 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,
+ )
+
+ max_procs = utils.max_procs() if max_procs == 0 else max_procs
+
if max_procs == 1:
for r in commands:
patched_run_command(r)
diff --git a/patternutils/commands/tln.py b/patternutils/commands/tln.py
index 295cda9..878f556 100644
--- a/patternutils/commands/tln.py
+++ b/patternutils/commands/tln.py
@@ -8,8 +8,7 @@ from .. import config, template, utils
def link_file(old_filename: str, new_filename: str, symbolic: bool) -> None:
- dirs = os.path.dirname(new_filename)
- if dirs:
+ if dirs := os.path.dirname(new_filename):
os.makedirs(dirs, exist_ok=True)
if symbolic:
@@ -98,19 +97,14 @@ def tln(
key: str,
stream: bool,
abort_on_path_exist: bool,
- dry_run,
- bool,
+ dry_run: bool,
verbose: bool,
) -> None:
- try:
- if not stream:
- data = json.load(read_from) # type: ignore
- else:
- data = [json.loads(line) for line in read_from]
- except json.JSONDecodeError:
- click.secho("JSON not valid!", fg="red", err=True)
- raise SystemExit
+ if stream:
+ data = map(utils.json_loads, read_from) # type: ignore
+ else:
+ data = utils.json_loads(read_from.read()) # type: ignore
if use_predefined_template:
try:
@@ -134,9 +128,10 @@ def tln(
if editor:
try:
- new_filenames = utils.edit(new_filenames)
+ if (temporary_filenames := utils.edit(new_filenames)) is not None:
+ new_filenames = temporary_filenames
except RuntimeError:
- click.secho("You edit is not parseable!", fg="red", err=True)
+ click.secho("Your edit is not parseable!", fg="red", err=True)
raise SystemExit
has_existing = False
@@ -151,10 +146,7 @@ def tln(
if has_existing and abort_on_path_exist:
raise SystemExit
- if force:
- files_to_move = new_filenames.items()
- else:
- files_to_move = non_existing.items()
+ files_to_move = new_filenames.items() if force else non_existing.items()
for old, new in files_to_move:
if verbose or interactive:
diff --git a/patternutils/commands/tmv.py b/patternutils/commands/tmv.py
index 3e92342..fa7105e 100644
--- a/patternutils/commands/tmv.py
+++ b/patternutils/commands/tmv.py
@@ -92,21 +92,17 @@ def tmv(
"""Rename files based on the OUTPUT_PATTERN.
- It reads a JSON list containing objects or a stream of objects
- (if --stream option is defined). The object needs to have a
- key named as the option KEY (default: _abspath), which points
- to the file to move. The rest of the keys will be supplied to
- the templating engine to generate the new filename.
+ 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.
"""
- try:
- if not stream:
- data = json.load(read_from) # type: ignore
- else:
- data = [json.loads(line) for line in read_from]
- except json.JSONDecodeError:
- click.secho("JSON not valid!", fg="red", err=True)
- raise SystemExit
+ if stream:
+ data = map(utils.json_loads, read_from) # type: ignore
+ else:
+ data = utils.json_loads(read_from.read()) # type: ignore
if use_predefined_template:
try:
@@ -130,7 +126,8 @@ def tmv(
if editor:
try:
- new_filenames = utils.edit(new_filenames)
+ if (temporary_filenames := utils.edit(new_filenames)) is not None:
+ new_filenames = temporary_filenames
except RuntimeError:
click.secho("Your edit is not parseable!", fg="red", err=True)
raise SystemExit
@@ -147,10 +144,7 @@ def tmv(
if has_existing and abort_on_path_exist:
raise SystemExit
- if force:
- files_to_move = new_filenames.items()
- else:
- files_to_move = non_existing.items()
+ files_to_move = new_filenames.items() if force else non_existing.items()
for old, new in files_to_move:
if verbose or interactive:
diff --git a/patternutils/config.py b/patternutils/config.py
index 319f035..f999b2e 100644
--- a/patternutils/config.py
+++ b/patternutils/config.py
@@ -1,7 +1,7 @@
import glob
import os.path
import re
-from typing import List, Optional, Tuple
+from typing import Optional
import click
@@ -11,7 +11,7 @@ REGEX_PROGRAMS = ("pmatch",)
TEMPLATE_PROGRAMS = ("tmv", "targs", "tln")
-def load_template(template_name: str, command_name: Optional[str]) -> Tuple[str, str]:
+def load_template(template_name: str, command_name: Optional[str]) -> tuple[str, str]:
"""Load a template from the config directory."""
files = find_templates(template_name, command_name)
diff --git a/patternutils/match.py b/patternutils/match.py
index 80f9553..fc6a805 100644
--- a/patternutils/match.py
+++ b/patternutils/match.py
@@ -1,9 +1,8 @@
-from typing import Dict, Pattern
+from typing import Pattern
-def apply(subject: str, regex_pattern: Pattern[str]) -> Dict[str, str]:
- match = regex_pattern.search(subject)
- if match is not None:
+def apply(subject: str, regex_pattern: Pattern[str]) -> dict[str, str]:
+ if (match := regex_pattern.search(subject)) is not None:
groupdict = match.groupdict()
groupdict["_subject"] = subject
return groupdict
diff --git a/patternutils/template.py b/patternutils/template.py
index 53746a2..f0b3c43 100644
--- a/patternutils/template.py
+++ b/patternutils/template.py
@@ -5,8 +5,8 @@ try:
except ImportError:
jinja_feature = False
else:
+ import os.path
import shlex
-
from datetime import datetime
jinja_feature = True
@@ -27,11 +27,19 @@ else:
def quote(value: str) -> str:
return shlex.quote(value)
+ def splitpath(value: str) -> tuple[str, str]:
+ return os.path.split(value)
+
+ def splitext(value: str) -> tuple[str, str]:
+ 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, *, engine: str = "python") -> Callable[..., str]:
diff --git a/patternutils/utils.py b/patternutils/utils.py
index 266f3b9..0395110 100644
--- a/patternutils/utils.py
+++ b/patternutils/utils.py
@@ -1,6 +1,6 @@
-import os
import json
-from typing import Dict, Any
+import os
+from typing import Any, Generator, Optional, Union
import click
@@ -10,30 +10,58 @@ def max_procs() -> int:
return 1 if cpu_count is None else cpu_count * 2
-def edit(data: Dict[str, str]) -> Dict[str, str]:
+def edit(data: Union[dict[str, str], list[str]]) -> Optional[dict[str, str]]:
longest = len(max(data, key=len))
- temporary = [
- "{}\t{}\n".format(key.ljust(longest), value) for key, value in data.items()
- ]
- edited = click.edit("".join(temporary))
- if edited 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
- else:
- raise RuntimeError
+ if isinstance(data, dict):
+ temporary = [f"{key.ljust(longest)}\t{value}\n" for key, value in data.items()]
else:
- return data
+ temporary = [f"{value.ljust(longest)}\t{value}\n" for value in 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
+ else:
+ raise RuntimeError
+ else:
+ return None
+ except click.UsageError:
+ click.secho("The editor could not be launced!", fg="red", err=True)
+ raise SystemExit
-def json_dumps(data: Any, humand_readable: bool = False) -> str:
- if humand_readable:
- return json.dumps(data, indent=4, sort_keys=True)
- else:
- return json.dumps(data, separators=(",", ":"))
+
+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)
+
+
+def json_loads(data: Any) -> Any:
+ try:
+ return json.loads(data)
+ except json.JSONDecodeError:
+ click.secho("JSON not valid!", fg="red", err=True)
+ raise SystemExit
+
+
+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
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..486a87d
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,335 @@
+[[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
new file mode 100644
index 0000000..4a340da
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,38 @@
+[tool.poetry]
+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"
+repository = "https://projects.metalgamer.eu/patternutils"
+
+[tool.poetry.scripts]
+pmatch = "patternutils.commands.pmatch:pmatch"
+targs = "patternutils.commands.targs:targs"
+tmv = "patternutils.commands.tmv:tmv"
+tln = "patternutils.commands.tln:tln"
+patterncfg = "patternutils.commands.patterncfg:patterncfg"
+prss = "patternutils.commands.prss:prss"
+pjson = "patternutils.commands.pjson:pjson"
+
+[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}
+
+[tool.poetry.dev-dependencies]
+mypy = "^0.930"
+black = "^21.12b0"
+isort = "^5.10.1"
+vermin = "^1.3.2"
+
+[tool.poetry.extras]
+jinja2 = ["Jinja2"]
+pygments = ["Pygments"]
+prss = ["feedparser"]
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 01156a3..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Click==7.0
-Jinja2==2.10.3
-MarkupSafe==1.1.1
-Pygments==2.4.2
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 95a892f..0000000
--- a/setup.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-from setuptools import find_packages, setup
-
-NAME = "patternutils"
-DESCRIPTION = "Combine regular expressions with template engines in a powerful matter."
-URL = "https://projects.metalgamer.eu/patternutils"
-EMAIL = "dennis.fink@c3l.lu"
-AUTHOR = "Dennis Fink"
-
-VERSION = "0.0.8"
-ENTRY_POINTS = {
- "console_scripts": [
- "pmatch=patternutils.commands.pmatch:pmatch",
- "targs=patternutils.commands.targs:targs",
- "tmv=patternutils.commands.tmv:tmv",
- "tln=patternutils.commands.tln:tln",
- "patterncfg=patternutils.commands.patterncfg:patterncfg",
- ],
-}
-
-
-REQUIRED = [
- "click==7.0",
-]
-
-OPTIONAL = {
- "jinja2": ["Jinja2==2.10.1"],
- "pygments": ["Pygments==2.4.2"],
-}
-
-setup(
- name=NAME,
- version=VERSION,
- description=DESCRIPTION,
- author=AUTHOR,
- author_email=EMAIL,
- url=URL,
- packages=find_packages(exclude=("tests",)),
- entry_points=ENTRY_POINTS,
- install_requires=REQUIRED,
- extras_requires=OPTIONAL,
- include_package_data=True,
- license="GPLv3+",
- classifiers=[
- # Trove classifiers
- # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
- "Programming Language :: Python",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: Implementation :: CPython",
- ],
-)