about summary refs log tree commit diff stats
path: root/pkgs/by-name/up
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-05-23 13:26:22 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-05-23 13:26:22 +0200
commit204731c0a69136c9cebcb54f1afecf5145e26bbe (patch)
treefc9132e5dc74e4a8e1327cdd411839a90f9410aa /pkgs/by-name/up
parentrefactor(sys): Modularize and move to `modules/system` or `pkgs` (diff)
downloadnixos-config-204731c0a69136c9cebcb54f1afecf5145e26bbe.tar.gz
nixos-config-204731c0a69136c9cebcb54f1afecf5145e26bbe.zip
refactor(pkgs): Categorize into `by-name` shards
This might not be the perfect way to organize a package set --
especially if the set is not nearly the size of nixpkgs -- but it is
_at_ least a way of organization.
Diffstat (limited to 'pkgs/by-name/up')
-rw-r--r--pkgs/by-name/up/update-sys/package.nix29
-rwxr-xr-xpkgs/by-name/up/update-sys/update-sys.sh85
-rw-r--r--pkgs/by-name/up/update-vim-plugins/.envrc1
-rwxr-xr-xpkgs/by-name/up/update-vim-plugins/check-duplicates.sh43
-rw-r--r--pkgs/by-name/up/update-vim-plugins/flake.lock61
-rw-r--r--pkgs/by-name/up/update-vim-plugins/flake.nix24
-rw-r--r--pkgs/by-name/up/update-vim-plugins/package.nix47
-rw-r--r--pkgs/by-name/up/update-vim-plugins/poetry.lock680
-rw-r--r--pkgs/by-name/up/update-vim-plugins/pyproject.toml45
-rwxr-xr-xpkgs/by-name/up/update-vim-plugins/update.sh5
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__init__.py0
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__main__.py15
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/cleanup.py100
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/helpers.py61
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/nix.py121
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/plugin.py182
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/spec.py143
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/__init__.py0
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/fixtures.py44
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_nix.py32
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_plugin.py144
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_spec.py136
-rw-r--r--pkgs/by-name/up/update-vim-plugins/update_vim_plugins/update.py212
23 files changed, 2210 insertions, 0 deletions
diff --git a/pkgs/by-name/up/update-sys/package.nix b/pkgs/by-name/up/update-sys/package.nix
new file mode 100644
index 00000000..8777f82d
--- /dev/null
+++ b/pkgs/by-name/up/update-sys/package.nix
@@ -0,0 +1,29 @@
+{
+  sysLib,
+  git,
+  nixos-rebuild,
+  sudo,
+  openssh,
+  coreutils,
+  mktemp,
+  gnugrep,
+  gnused,
+  systemd,
+}:
+sysLib.writeShellScript {
+  name = "update-sys";
+  src = ./update-sys.sh;
+  generateCompletions = true;
+  keepPath = false;
+  dependencies = [
+    git
+    nixos-rebuild
+    sudo
+    openssh
+    coreutils
+    mktemp
+    gnugrep
+    gnused
+    systemd
+  ];
+}
diff --git a/pkgs/by-name/up/update-sys/update-sys.sh b/pkgs/by-name/up/update-sys/update-sys.sh
new file mode 100755
index 00000000..d28247f6
--- /dev/null
+++ b/pkgs/by-name/up/update-sys/update-sys.sh
@@ -0,0 +1,85 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+help() {
+    cat <<EOF
+This is a NixOS System flake update manager.
+
+USAGE:
+    $NAME [--branch <branchname>] [--help]
+
+OPTIONS:
+    --branch | -b  BRANCHNAME
+                                select a branch to update from.
+
+    --mode   | -m  MODE
+                                select a mode to update with
+
+    --help   | -h
+                                output this help.
+ARGUMENTS:
+    BRANCHNAME := [[ git branch --list --format '%(refname:short)' ]]
+                                The name of the branch to deploy the config from
+
+    MODE := switch|boot|test|build|dry-build|dry-activate|edit|repl|build-vm|build-vm-with-bootloader
+                                See the 'nixos-rebuild' manpage for more information about these modes.
+EOF
+    exit "$1"
+}
+default_branch=$(mktmp)
+BRANCH=""
+
+while [ "$#" -gt 0 ]; do
+    case "$1" in
+    "--help" | "-h")
+        help 0
+        ;;
+    "--branch" | "-b")
+        if [ -n "$2" ]; then
+            BRANCH="$2"
+        else
+            error "$1 requires an argument"
+            help 1
+        fi
+        shift 2
+        ;;
+    "--mode" | "-m")
+        if [ -n "$2" ]; then
+            MODE="$2"
+        else
+            error "$1 requires an argument"
+            help 1
+        fi
+        shift 2
+        ;;
+    *)
+        error "the option $1 does not exist!"
+        help 1
+        ;;
+    esac
+done
+
+cd /etc/nixos || die "No /etc/nixos"
+msg "Starting system update..."
+git remote update origin --prune >/dev/null 2>&1
+if ! [ "$BRANCH" = "" ]; then
+    git switch "$BRANCH" >/dev/null 2>&1 && msg2 "Switched to branch '$BRANCH'"
+fi
+msg2 "Updating git repository..."
+git pull --rebase
+
+git remote show origin | grep 'HEAD' | cut -d':' -f2 | sed -e 's/^ *//g' -e 's/ *$//g' >"$default_branch" &
+
+msg2 "Updating system..."
+if [ -n "$MODE" ]; then
+    nixos-rebuild "$MODE"
+else
+    nixos-rebuild switch
+fi
+
+git switch "$(cat "$default_branch")" >/dev/null 2>&1 && msg2 "Switched to branch '$(cat "$default_branch")'"
+msg "Finished Update!"
+
+# vim: ft=sh
diff --git a/pkgs/by-name/up/update-vim-plugins/.envrc b/pkgs/by-name/up/update-vim-plugins/.envrc
new file mode 100644
index 00000000..3550a30f
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/pkgs/by-name/up/update-vim-plugins/check-duplicates.sh b/pkgs/by-name/up/update-vim-plugins/check-duplicates.sh
new file mode 100755
index 00000000..781b8aeb
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/check-duplicates.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+
+plugins="$(grep -E "^  [a-zA-Z-]+ =" ./pkgs/vim-plugins.nix | sed -E 's/^  ([a-zA-Z-]+) =.*$/\1/' | sort)"
+count=$(echo "$plugins" | uniq -d | wc -l)
+
+echo "duplicates count: $count"
+
+if [ "$count" -gt 0 ]; then
+    filtered_plugins=$(echo "$plugins" | uniq -d)
+
+    if [ "$1" == "check-only" ]; then
+        echo "$filtered_plugins"
+        exit 1
+    else
+        known_issues=$(gh issue list --state "open" --label "bot" --json "body" | jq -r ".[].body")
+
+        echo "known_issues: $known_issues"
+
+        # iterate over plugins we found missing and
+        # compare them to all open issues.
+        # We no matching issue was found, we create a new one
+        for f in $filtered_plugins; do # do not add " " here. It would break the plugin
+            found=false
+
+            for k in $known_issues; do
+                if [[ $f == "$k" ]]; then
+                    found=true
+                    break
+                fi
+            done
+
+            # test if matching issue was found
+            if ! $found; then
+                echo "Did not find an issue for $f. Creating a new one ..."
+                gh issue create --title "Detected broken plugin: $f" --label "bot" --body "$f"
+            else
+                echo "Issue for $f already exists"
+            fi
+        done
+    fi
+else
+    echo "No duplicates found"
+fi
diff --git a/pkgs/by-name/up/update-vim-plugins/flake.lock b/pkgs/by-name/up/update-vim-plugins/flake.lock
new file mode 100644
index 00000000..50494465
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/flake.lock
@@ -0,0 +1,61 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1715087517,
+        "narHash": "sha256-CLU5Tsg24Ke4+7sH8azHWXKd0CFd4mhLWfhYgUiDBpQ=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "b211b392b8486ee79df6cdfb1157ad2133427a29",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/pkgs/by-name/up/update-vim-plugins/flake.nix b/pkgs/by-name/up/update-vim-plugins/flake.nix
new file mode 100644
index 00000000..ef440af0
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/flake.nix
@@ -0,0 +1,24 @@
+{
+  description = "update_vim_plugins";
+
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+
+    flake-utils.url = "github:numtide/flake-utils";
+  };
+
+  outputs = {
+    self,
+    nixpkgs,
+    flake-utils,
+  }: (flake-utils.lib.eachDefaultSystem (system: let
+    pkgs = nixpkgs.legacyPackages.${system};
+  in {
+    devShells.default = pkgs.mkShell {
+      packages = [
+        pkgs.python3
+        pkgs.poetry
+      ];
+    };
+  }));
+}
diff --git a/pkgs/by-name/up/update-vim-plugins/package.nix b/pkgs/by-name/up/update-vim-plugins/package.nix
new file mode 100644
index 00000000..e74a29b1
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/package.nix
@@ -0,0 +1,47 @@
+{
+  python3,
+  # dependencies
+  nix,
+  alejandra,
+  nix-prefetch-git,
+}:
+python3.pkgs.buildPythonApplication {
+  pname = "update-vim-plugins";
+  version = "0.1.0";
+  format = "pyproject";
+
+  src = ./.;
+
+  # NOTE: The test are not really meant to work <2023-12-09>
+  doCheck = false;
+
+  nativeBuildInputs = [
+    python3.pkgs.poetry-core
+  ];
+  buildInputs = [
+    alejandra
+    nix-prefetch-git
+    nix
+  ];
+  propagatedBuildInputs = with python3.pkgs; [
+    requests
+    cleo
+    jsonpickle
+    dateparser
+  ];
+  nativeCheckInputs = with python3.pkgs; [
+    pytestCheckHook
+
+    pytest-cov
+    pytest-mock
+  ];
+  pytestFlagsArray = [
+    "--cov"
+    "update_vim_plugins"
+    "--cov-report"
+    "term-missing:skip-covered"
+    "--cov-fail-under"
+    "50"
+    "update_vim_plugins/tests"
+  ];
+}
diff --git a/pkgs/by-name/up/update-vim-plugins/poetry.lock b/pkgs/by-name/up/update-vim-plugins/poetry.lock
new file mode 100644
index 00000000..f4764b42
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/poetry.lock
@@ -0,0 +1,680 @@
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+
+[[package]]
+name = "certifi"
+version = "2024.2.2"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
+    {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.3.2"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+    {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+]
+
+[[package]]
+name = "cleo"
+version = "2.1.0"
+description = "Cleo allows you to create beautiful and testable command-line interfaces."
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+    {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"},
+    {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"},
+]
+
+[package.dependencies]
+crashtest = ">=0.4.1,<0.5.0"
+rapidfuzz = ">=3.0.0,<4.0.0"
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.5.1"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"},
+    {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"},
+    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"},
+    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"},
+    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"},
+    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"},
+    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"},
+    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"},
+    {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"},
+    {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"},
+    {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"},
+    {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"},
+    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"},
+    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"},
+    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"},
+    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"},
+    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"},
+    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"},
+    {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"},
+    {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"},
+    {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"},
+    {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"},
+    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"},
+    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"},
+    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"},
+    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"},
+    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"},
+    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"},
+    {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"},
+    {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"},
+    {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"},
+    {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"},
+    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"},
+    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"},
+    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"},
+    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"},
+    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"},
+    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"},
+    {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"},
+    {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"},
+    {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"},
+    {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"},
+    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"},
+    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"},
+    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"},
+    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"},
+    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"},
+    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"},
+    {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"},
+    {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"},
+    {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"},
+    {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"},
+]
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "crashtest"
+version = "0.4.1"
+description = "Manage Python errors with ease"
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+    {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"},
+    {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"},
+]
+
+[[package]]
+name = "dateparser"
+version = "1.2.0"
+description = "Date parsing library designed to parse dates from HTML pages"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"},
+    {file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"},
+]
+
+[package.dependencies]
+python-dateutil = "*"
+pytz = "*"
+regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27"
+tzlocal = "*"
+
+[package.extras]
+calendars = ["convertdate", "hijri-converter"]
+fasttext = ["fasttext"]
+langdetect = ["langdetect"]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.1"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+    {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "idna"
+version = "3.7"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+    {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+    {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "jsonpickle"
+version = "3.0.4"
+description = "Serialize any Python object to JSON"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "jsonpickle-3.0.4-py3-none-any.whl", hash = "sha256:04ae7567a14269579e3af66b76bda284587458d7e8a204951ca8f71a3309952e"},
+    {file = "jsonpickle-3.0.4.tar.gz", hash = "sha256:a1b14c8d6221cd8f394f2a97e735ea1d7edc927fbd135b26f2f8700657c8c62b"},
+]
+
+[package.extras]
+docs = ["furo", "rst.linker (>=1.9)", "sphinx"]
+packaging = ["build", "twine"]
+testing = ["bson", "ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-benchmark", "pytest-benchmark[histogram]", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-ruff (>=0.2.1)", "scikit-learn", "scipy", "scipy (>=1.9.3)", "simplejson", "sqlalchemy", "ujson"]
+
+[[package]]
+name = "packaging"
+version = "24.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
+    {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pytest"
+version = "7.4.4"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+    {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.1.0"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+    {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+]
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "pytest-mock"
+version = "3.14.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
+    {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
+]
+
+[package.dependencies]
+pytest = ">=6.2.5"
+
+[package.extras]
+dev = ["pre-commit", "pytest-asyncio", "tox"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+    {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+    {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pytz"
+version = "2024.1"
+description = "World timezone definitions, modern and historical"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
+    {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
+]
+
+[[package]]
+name = "rapidfuzz"
+version = "3.9.0"
+description = "rapid fuzzy string matching"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bd375c4830fee11d502dd93ecadef63c137ae88e1aaa29cc15031fa66d1e0abb"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:55e2c5076f38fc1dbaacb95fa026a3e409eee6ea5ac4016d44fb30e4cad42b20"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:488f74126904db6b1bea545c2f3567ea882099f4c13f46012fe8f4b990c683df"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3f2d1ea7cd57dfcd34821e38b4924c80a31bcf8067201b1ab07386996a9faee"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b11e602987bcb4ea22b44178851f27406fca59b0836298d0beb009b504dba266"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3083512e9bf6ed2bb3d25883922974f55e21ae7f8e9f4e298634691ae1aee583"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b33c6d4b3a1190bc0b6c158c3981535f9434e8ed9ffa40cf5586d66c1819fb4b"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcb95fde22f98e6d0480db8d6038c45fe2d18a338690e6f9bba9b82323f3469"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:08d8b49b3a4fb8572e480e73fcddc750da9cbb8696752ee12cca4bf8c8220d52"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e721842e6b601ebbeb8cc5e12c75bbdd1d9e9561ea932f2f844c418c31256e82"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7988363b3a415c5194ce1a68d380629247f8713e669ad81db7548eb156c4f365"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2d267d4c982ab7d177e994ab1f31b98ff3814f6791b90d35dda38307b9e7c989"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0bb28ab5300cf974c7eb68ea21125c493e74b35b1129e629533468b2064ae0a2"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-win32.whl", hash = "sha256:1b1f74997b6d94d66375479fa55f70b1c18e4d865d7afcd13f0785bfd40a9d3c"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:c56d2efdfaa1c642029f3a7a5bb76085c5531f7a530777be98232d2ce142553c"},
+    {file = "rapidfuzz-3.9.0-cp310-cp310-win_arm64.whl", hash = "sha256:6a83128d505cac76ea560bb9afcb3f6986e14e50a6f467db9a31faef4bd9b347"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e2218d62ab63f3c5ad48eced898854d0c2c327a48f0fb02e2288d7e5332a22c8"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:36bf35df2d6c7d5820da20a6720aee34f67c15cd2daf8cf92e8141995c640c25"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:905b01a9b633394ff6bb5ebb1c5fd660e0e180c03fcf9d90199cc6ed74b87cf7"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33cfabcb7fd994938a6a08e641613ce5fe46757832edc789c6a5602e7933d6fa"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1179dcd3d150a67b8a678cd9c84f3baff7413ff13c9e8fe85e52a16c97e24c9b"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47d97e28c42f1efb7781993b67c749223f198f6653ef177a0c8f2b1c516efcaf"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28da953eb2ef9ad527e536022da7afff6ace7126cdd6f3e21ac20f8762e76d2c"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:182b4e11de928fb4834e8f8b5ecd971b5b10a86fabe8636ab65d3a9b7e0e9ca7"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c74f2da334ce597f31670db574766ddeaee5d9430c2c00e28d0fbb7f76172036"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:014ac55b03f4074f903248ded181f3000f4cdbd134e6155cbf643f0eceb4f70f"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c4ef34b2ddbf448f1d644b4ec6475df8bbe5b9d0fee173ff2e87322a151663bd"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fc02157f521af15143fae88f92ef3ddcc4e0cff05c40153a9549dc0fbdb9adb3"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ff08081c49b18ba253a99e6a47f492e6ee8019e19bbb6ddc3ed360cd3ecb2f62"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-win32.whl", hash = "sha256:b9bf90b3d96925cbf8ef44e5ee3cf39ef0c422f12d40f7a497e91febec546650"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5d5684f54d82d9b0cf0b2701e55a630527a9c3dd5ddcf7a2e726a475ac238f2"},
+    {file = "rapidfuzz-3.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:a2de844e0e971d7bd8aa41284627dbeacc90e750b90acfb016836553c7a63192"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f81fe99a69ac8ee3fd905e70c62f3af033901aeb60b69317d1d43d547b46e510"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:633b9d03fc04abc585c197104b1d0af04b1f1db1abc99f674d871224cd15557a"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab872cb57ae97c54ba7c71a9e3c9552beb57cb907c789b726895576d1ea9af6f"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdd8c15c3a14e409507fdf0c0434ec481d85c6cbeec8bdcd342a8cd1eda03825"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2444d8155d9846f206e2079bb355b85f365d9457480b0d71677a112d0a7f7128"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f83bd3d01f04061c3660742dc85143a89d49fd23eb31eccbf60ad56c4b955617"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ca799f882364e69d0872619afb19efa3652b7133c18352e4a3d86a324fb2bb1"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6993d361f28b9ef5f0fa4e79b8541c2f3507be7471b9f9cb403a255e123b31e1"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:170822a1b1719f02b58e3dce194c8ad7d4c5b39be38c0fdec603bd19c6f9cf81"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e86e39c1c1a0816ceda836e6f7bd3743b930cbc51a43a81bb433b552f203f25"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:731269812ea837e0b93d913648e404736407408e33a00b75741e8f27c590caa2"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8e5ff882d3a3d081157ceba7e0ebc7fac775f95b08cbb143accd4cece6043819"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2003071aa633477a01509890c895f9ef56cf3f2eaa72c7ec0b567f743c1abcba"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-win32.whl", hash = "sha256:13857f9070600ea1f940749f123b02d0b027afbaa45e72186df0f278915761d0"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:134b7098ac109834eeea81424b6822f33c4c52bf80b81508295611e7a21be12a"},
+    {file = "rapidfuzz-3.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:2a96209f046fe328be30fc43f06e3d4b91f0d5b74e9dcd627dbfd65890fa4a5e"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:544b0bf9d17170720809918e9ccd0d482d4a3a6eca35630d8e1459f737f71755"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d536f8beb8dd82d6efb20fe9f82c2cfab9ffa0384b5d184327e393a4edde91d"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:30f7609da871510583f87484a10820b26555a473a90ab356cdda2f3b4456256c"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f4a2468432a1db491af6f547fad8f6d55fa03e57265c2f20e5eaceb68c7907e"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a7ec4676242c8a430509cff42ce98bca2fbe30188a63d0f60fdcbfd7e84970"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dcb523243e988c849cf81220164ec3bbed378a699e595a8914fffe80596dc49f"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4eea3bf72c4fe68e957526ffd6bcbb403a21baa6b3344aaae2d3252313df6199"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4514980a5d204c076dd5b756960f6b1b7598f030009456e6109d76c4c331d03c"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9a06a99f1335fe43464d7121bc6540de7cd9c9475ac2025babb373fe7f27846b"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6c1ed63345d1581c39d4446b1a8c8f550709656ce2a3c88c47850b258167f3c2"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cd2e6e97daf17ebb3254285cf8dd86c60d56d6cf35c67f0f9a557ef26bd66290"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9bc0f7e6256a9c668482c41c8a3de5d0aa12e8ca346dcc427b97c7edb82cba48"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c09f4e87e82a164c9db769474bc61f8c8b677f2aeb0234b8abac73d2ecf9799"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-win32.whl", hash = "sha256:e65b8f7921bf60cbb207c132842a6b45eefef48c4c3b510eb16087d6c08c70af"},
+    {file = "rapidfuzz-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9d6478957fb35c7844ad08f2442b62ba76c1857a56370781a707eefa4f4981e1"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65d9250a4b0bf86320097306084bc3ca479c8f5491927c170d018787793ebe95"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:47b7c0840afa724db3b1a070bc6ed5beab73b4e659b1d395023617fc51bf68a2"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a16c48c6df8fb633efbbdea744361025d01d79bca988f884a620e63e782fe5b"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48105991ff6e4a51c7f754df500baa070270ed3d41784ee0d097549bc9fcb16d"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a7f273906b3c7cc6d63a76e088200805947aa0bc1ada42c6a0e582e19c390d7"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c396562d304e974b4b0d5cd3afc4f92c113ea46a36e6bc62e45333d6aa8837e"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68da1b70458fea5290ec9a169fcffe0c17ff7e5bb3c3257e63d7021a50601a8e"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c5b8f9a7b177af6ce7c6ad5b95588b4b73e37917711aafa33b2e79ee80fe709"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3c42a238bf9dd48f4ccec4c6934ac718225b00bb3a438a008c219e7ccb3894c7"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a365886c42177b2beab475a50ba311b59b04f233ceaebc4c341f6f91a86a78e2"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ce897b5dafb7fb7587a95fe4d449c1ea0b6d9ac4462fbafefdbbeef6eee4cf6a"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:413ac49bae291d7e226a5c9be65c71b2630b3346bce39268d02cb3290232e4b7"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8982fc3bd49d55a91569fc8a3feba0de4cef0b391ff9091be546e9df075b81"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-win32.whl", hash = "sha256:3904d0084ab51f82e9f353031554965524f535522a48ec75c30b223eb5a0a488"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:3733aede16ea112728ffeafeb29ccc62e095ed8ec816822fa2a82e92e2c08696"},
+    {file = "rapidfuzz-3.9.0-cp39-cp39-win_arm64.whl", hash = "sha256:fc4e26f592b51f97acf0a3f8dfed95e4d830c6a8fbf359361035df836381ab81"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e33362e98c7899b5f60dcb06ada00acd8673ce0d59aefe9a542701251fd00423"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb67cf43ad83cb886cbbbff4df7dcaad7aedf94d64fca31aea0da7d26684283c"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2e106cc66453bb80d2ad9c0044f8287415676df5c8036d737d05d4b9cdbf8e"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1256915f7e7a5cf2c151c9ac44834b37f9bd1c97e8dec6f936884f01b9dfc7d"},
+    {file = "rapidfuzz-3.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ae643220584518cbff8bf2974a0494d3e250763af816b73326a512c86ae782ce"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:491274080742110427f38a6085bb12dffcaff1eef12dccf9e8758398c7e3957e"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bc5559b9b94326922c096b30ae2d8e5b40b2e9c2c100c2cc396ad91bcb84d30"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:849160dc0f128acb343af514ca827278005c1d00148d025e4035e034fc2d8c7f"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:623883fb78e692d54ed7c43b09beec52c6685f10a45a7518128e25746667403b"},
+    {file = "rapidfuzz-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d20ab9abc7e19767f1951772a6ab14cb4eddd886493c2da5ee12014596ad253f"},
+    {file = "rapidfuzz-3.9.0.tar.gz", hash = "sha256:b182f0fb61f6ac435e416eb7ab330d62efdbf9b63cf0c7fa12d1f57c2eaaf6f3"},
+]
+
+[package.extras]
+full = ["numpy"]
+
+[[package]]
+name = "regex"
+version = "2024.4.28"
+description = "Alternative regular expression module, to replace re."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "regex-2024.4.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd196d056b40af073d95a2879678585f0b74ad35190fac04ca67954c582c6b61"},
+    {file = "regex-2024.4.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8bb381f777351bd534462f63e1c6afb10a7caa9fa2a421ae22c26e796fe31b1f"},
+    {file = "regex-2024.4.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:47af45b6153522733aa6e92543938e97a70ce0900649ba626cf5aad290b737b6"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d6a550425cc51c656331af0e2b1651e90eaaa23fb4acde577cf15068e2e20f"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf29304a8011feb58913c382902fde3395957a47645bf848eea695839aa101b7"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92da587eee39a52c91aebea8b850e4e4f095fe5928d415cb7ed656b3460ae79a"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6277d426e2f31bdbacb377d17a7475e32b2d7d1f02faaecc48d8e370c6a3ff31"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28e1f28d07220c0f3da0e8fcd5a115bbb53f8b55cecf9bec0c946eb9a059a94c"},
+    {file = "regex-2024.4.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aaa179975a64790c1f2701ac562b5eeb733946eeb036b5bcca05c8d928a62f10"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6f435946b7bf7a1b438b4e6b149b947c837cb23c704e780c19ba3e6855dbbdd3"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:19d6c11bf35a6ad077eb23852827f91c804eeb71ecb85db4ee1386825b9dc4db"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:fdae0120cddc839eb8e3c15faa8ad541cc6d906d3eb24d82fb041cfe2807bc1e"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e672cf9caaf669053121f1766d659a8813bd547edef6e009205378faf45c67b8"},
+    {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f57515750d07e14743db55d59759893fdb21d2668f39e549a7d6cad5d70f9fea"},
+    {file = "regex-2024.4.28-cp310-cp310-win32.whl", hash = "sha256:a1409c4eccb6981c7baabc8888d3550df518add6e06fe74fa1d9312c1838652d"},
+    {file = "regex-2024.4.28-cp310-cp310-win_amd64.whl", hash = "sha256:1f687a28640f763f23f8a9801fe9e1b37338bb1ca5d564ddd41619458f1f22d1"},
+    {file = "regex-2024.4.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84077821c85f222362b72fdc44f7a3a13587a013a45cf14534df1cbbdc9a6796"},
+    {file = "regex-2024.4.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45d4503de8f4f3dc02f1d28a9b039e5504a02cc18906cfe744c11def942e9eb"},
+    {file = "regex-2024.4.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:457c2cd5a646dd4ed536c92b535d73548fb8e216ebee602aa9f48e068fc393f3"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b51739ddfd013c6f657b55a508de8b9ea78b56d22b236052c3a85a675102dc6"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:459226445c7d7454981c4c0ce0ad1a72e1e751c3e417f305722bbcee6697e06a"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:670fa596984b08a4a769491cbdf22350431970d0112e03d7e4eeaecaafcd0fec"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe00f4fe11c8a521b173e6324d862ee7ee3412bf7107570c9b564fe1119b56fb"},
+    {file = "regex-2024.4.28-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36f392dc7763fe7924575475736bddf9ab9f7a66b920932d0ea50c2ded2f5636"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:23a412b7b1a7063f81a742463f38821097b6a37ce1e5b89dd8e871d14dbfd86b"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f1d6e4b7b2ae3a6a9df53efbf199e4bfcff0959dbdb5fd9ced34d4407348e39a"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:499334ad139557de97cbc4347ee921c0e2b5e9c0f009859e74f3f77918339257"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:0940038bec2fe9e26b203d636c44d31dd8766abc1fe66262da6484bd82461ccf"},
+    {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:66372c2a01782c5fe8e04bff4a2a0121a9897e19223d9eab30c54c50b2ebeb7f"},
+    {file = "regex-2024.4.28-cp311-cp311-win32.whl", hash = "sha256:c77d10ec3c1cf328b2f501ca32583625987ea0f23a0c2a49b37a39ee5c4c4630"},
+    {file = "regex-2024.4.28-cp311-cp311-win_amd64.whl", hash = "sha256:fc0916c4295c64d6890a46e02d4482bb5ccf33bf1a824c0eaa9e83b148291f90"},
+    {file = "regex-2024.4.28-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:08a1749f04fee2811c7617fdd46d2e46d09106fa8f475c884b65c01326eb15c5"},
+    {file = "regex-2024.4.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b8eb28995771c087a73338f695a08c9abfdf723d185e57b97f6175c5051ff1ae"},
+    {file = "regex-2024.4.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd7ef715ccb8040954d44cfeff17e6b8e9f79c8019daae2fd30a8806ef5435c0"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb0315a2b26fde4005a7c401707c5352df274460f2f85b209cf6024271373013"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fc053228a6bd3a17a9b0a3f15c3ab3cf95727b00557e92e1cfe094b88cc662"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fe9739a686dc44733d52d6e4f7b9c77b285e49edf8570754b322bca6b85b4cc"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74fcf77d979364f9b69fcf8200849ca29a374973dc193a7317698aa37d8b01c"},
+    {file = "regex-2024.4.28-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:965fd0cf4694d76f6564896b422724ec7b959ef927a7cb187fc6b3f4e4f59833"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2fef0b38c34ae675fcbb1b5db760d40c3fc3612cfa186e9e50df5782cac02bcd"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bc365ce25f6c7c5ed70e4bc674f9137f52b7dd6a125037f9132a7be52b8a252f"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ac69b394764bb857429b031d29d9604842bc4cbfd964d764b1af1868eeebc4f0"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:144a1fc54765f5c5c36d6d4b073299832aa1ec6a746a6452c3ee7b46b3d3b11d"},
+    {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2630ca4e152c221072fd4a56d4622b5ada876f668ecd24d5ab62544ae6793ed6"},
+    {file = "regex-2024.4.28-cp312-cp312-win32.whl", hash = "sha256:7f3502f03b4da52bbe8ba962621daa846f38489cae5c4a7b5d738f15f6443d17"},
+    {file = "regex-2024.4.28-cp312-cp312-win_amd64.whl", hash = "sha256:0dd3f69098511e71880fb00f5815db9ed0ef62c05775395968299cb400aeab82"},
+    {file = "regex-2024.4.28-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:374f690e1dd0dbdcddea4a5c9bdd97632cf656c69113f7cd6a361f2a67221cb6"},
+    {file = "regex-2024.4.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f87ae6b96374db20f180eab083aafe419b194e96e4f282c40191e71980c666"},
+    {file = "regex-2024.4.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5dbc1bcc7413eebe5f18196e22804a3be1bfdfc7e2afd415e12c068624d48247"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f85151ec5a232335f1be022b09fbbe459042ea1951d8a48fef251223fc67eee1"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57ba112e5530530fd175ed550373eb263db4ca98b5f00694d73b18b9a02e7185"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:224803b74aab56aa7be313f92a8d9911dcade37e5f167db62a738d0c85fdac4b"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a54a047b607fd2d2d52a05e6ad294602f1e0dec2291152b745870afc47c1397"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a2a512d623f1f2d01d881513af9fc6a7c46e5cfffb7dc50c38ce959f9246c94"},
+    {file = "regex-2024.4.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c06bf3f38f0707592898428636cbb75d0a846651b053a1cf748763e3063a6925"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1031a5e7b048ee371ab3653aad3030ecfad6ee9ecdc85f0242c57751a05b0ac4"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7a353ebfa7154c871a35caca7bfd8f9e18666829a1dc187115b80e35a29393e"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7e76b9cfbf5ced1aca15a0e5b6f229344d9b3123439ffce552b11faab0114a02"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5ce479ecc068bc2a74cb98dd8dba99e070d1b2f4a8371a7dfe631f85db70fe6e"},
+    {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d77b6f63f806578c604dca209280e4c54f0fa9a8128bb8d2cc5fb6f99da4150"},
+    {file = "regex-2024.4.28-cp38-cp38-win32.whl", hash = "sha256:d84308f097d7a513359757c69707ad339da799e53b7393819ec2ea36bc4beb58"},
+    {file = "regex-2024.4.28-cp38-cp38-win_amd64.whl", hash = "sha256:2cc1b87bba1dd1a898e664a31012725e48af826bf3971e786c53e32e02adae6c"},
+    {file = "regex-2024.4.28-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7413167c507a768eafb5424413c5b2f515c606be5bb4ef8c5dee43925aa5718b"},
+    {file = "regex-2024.4.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:108e2dcf0b53a7c4ab8986842a8edcb8ab2e59919a74ff51c296772e8e74d0ae"},
+    {file = "regex-2024.4.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f1c5742c31ba7d72f2dedf7968998730664b45e38827637e0f04a2ac7de2f5f1"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecc6148228c9ae25ce403eade13a0961de1cb016bdb35c6eafd8e7b87ad028b1"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7d893c8cf0e2429b823ef1a1d360a25950ed11f0e2a9df2b5198821832e1947"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4290035b169578ffbbfa50d904d26bec16a94526071ebec3dadbebf67a26b25e"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a22ae1cfd82e4ffa2066eb3390777dc79468f866f0625261a93e44cdf6482b"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd24fd140b69f0b0bcc9165c397e9b2e89ecbeda83303abf2a072609f60239e2"},
+    {file = "regex-2024.4.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:39fb166d2196413bead229cd64a2ffd6ec78ebab83fff7d2701103cf9f4dfd26"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9301cc6db4d83d2c0719f7fcda37229691745168bf6ae849bea2e85fc769175d"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c3d389e8d76a49923683123730c33e9553063d9041658f23897f0b396b2386f"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:99ef6289b62042500d581170d06e17f5353b111a15aa6b25b05b91c6886df8fc"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b91d529b47798c016d4b4c1d06cc826ac40d196da54f0de3c519f5a297c5076a"},
+    {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:43548ad74ea50456e1c68d3c67fff3de64c6edb85bcd511d1136f9b5376fc9d1"},
+    {file = "regex-2024.4.28-cp39-cp39-win32.whl", hash = "sha256:05d9b6578a22db7dedb4df81451f360395828b04f4513980b6bd7a1412c679cc"},
+    {file = "regex-2024.4.28-cp39-cp39-win_amd64.whl", hash = "sha256:3986217ec830c2109875be740531feb8ddafe0dfa49767cdcd072ed7e8927962"},
+    {file = "regex-2024.4.28.tar.gz", hash = "sha256:83ab366777ea45d58f72593adf35d36ca911ea8bd838483c1823b883a121b0e4"},
+]
+
+[[package]]
+name = "requests"
+version = "2.31.0"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
+    {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "tzdata"
+version = "2024.1"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+files = [
+    {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+    {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+]
+
+[[package]]
+name = "tzlocal"
+version = "5.2"
+description = "tzinfo object for the local timezone"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"},
+    {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"},
+]
+
+[package.dependencies]
+tzdata = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
+
+[[package]]
+name = "urllib3"
+version = "2.2.1"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
+    {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.10"
+content-hash = "f65cd66387236673e2a5afb3b2a75362c97815cdde592a86712737fb9ca71695"
diff --git a/pkgs/by-name/up/update-vim-plugins/pyproject.toml b/pkgs/by-name/up/update-vim-plugins/pyproject.toml
new file mode 100644
index 00000000..38caf76d
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/pyproject.toml
@@ -0,0 +1,45 @@
+[tool.poetry]
+name = "update_vim_plugins"
+version = "0.1.0"
+description = ""
+authors = ["Your Name <you@example.com>"]
+packages = [{ include = "update_vim_plugins" }]
+
+[tool.poetry.scripts]
+update-vim-plugins = "update_vim_plugins.__main__:main"
+
+[tool.poetry.dependencies]
+python = "^3.10"
+requests = "^2.28.2"
+cleo = "^2.0.1"
+jsonpickle = "*"
+dateparser = "^1.1.8"
+
+[tool.poetry.group.test.dependencies]
+pytest-cov = "^4.0.0"
+pytest = "^7.3.1"
+pytest-mock = "^3.10.0"
+
+[tool.poetry.group.dev]
+optional = true
+
+[tool.poetry.group.dev.dependencies]
+# black = "^23.3.0"
+# ruff-lsp = "^0.0.24"
+# mypy = "^1.2.0"
+# types-requests = "^2.28.11.17"
+# isort = "^5.12.0"
+# ruff = "^0.0.262"
+
+[tool.isort]
+profile = "black"
+
+[tool.black]
+line-length = 120
+
+[tool.ruff]
+line-length = 120
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/pkgs/by-name/up/update-vim-plugins/update.sh b/pkgs/by-name/up/update-vim-plugins/update.sh
new file mode 100755
index 00000000..1bad12a9
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env sh
+
+poetry update --lock
+
+# vim: ft=sh
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__init__.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__init__.py
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__main__.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__main__.py
new file mode 100644
index 00000000..a8d9e06f
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/__main__.py
@@ -0,0 +1,15 @@
+from cleo.application import Application
+
+from .update import UpdateCommand
+from .cleanup import CleanUpCommand
+
+
+def main():
+    application = Application()
+    application.add(UpdateCommand())
+    application.add(CleanUpCommand())
+    application.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/cleanup.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/cleanup.py
new file mode 100644
index 00000000..fd313ed0
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/cleanup.py
@@ -0,0 +1,100 @@
+from cleo.commands.command import Command
+from cleo.helpers import argument
+
+from .helpers import read_manifest_to_spec, read_blacklist_to_spec, write_manifest_from_spec
+
+
+class CleanUpCommand(Command):
+    name = "cleanup"
+    description = "Clean up manifest"
+    arguments = [argument("plug_dir", description="Path to the plugin directory", optional=False)]
+
+    def handle(self):
+        """Main command function"""
+
+        plug_dir = self.argument("plug_dir")
+        self.line("<comment>Checking manifest file</comment>")
+        # all cleaning up will be done during reading and writing automatically
+        manifest = read_manifest_to_spec(plug_dir)
+        blacklist = read_blacklist_to_spec(plug_dir)
+
+        new_manifest = [spec for spec in manifest if spec not in blacklist]
+
+        new_manifest_filterd = self.filter_renamed(new_manifest)
+
+        write_manifest_from_spec(new_manifest_filterd, plug_dir)
+
+        self.line("<comment>Done</comment>")
+
+    def filter_renamed(self, specs):
+        """Filter specs that define the same plugin (same owner and same repo) but with different properties.
+        This could be a different name, source, or branch
+        """
+
+        error = False
+        for i, p in enumerate(specs):
+            for p2 in specs:
+                same_owner = p.owner.lower() == p2.owner.lower()
+                same_repo = p.repo.lower() == p2.repo.lower()
+                different_specs = p != p2
+                marked_duplicate = p.marked_duplicate or p2.marked_duplicate
+
+                if same_owner and same_repo and different_specs and not marked_duplicate:
+                    self.line("<info>The following lines appear to define the same plugin</info>")
+
+                    p_props_defined = p.branch is not None or p.custom_name is not None
+                    p2_props_defined = p2.branch is not None or p2.custom_name is not None
+                    p_is_lower_case = p.owner == p.owner.lower() and p.name == p.name.lower()
+                    p2_is_lower_case = p2.owner == p2.owner.lower() and p2.name == p2.name.lower()
+
+                    # list of conditions for selecting p
+                    select_p = p_props_defined and not p2_props_defined or p2_is_lower_case and not p_is_lower_case
+                    # list of conditions for selecting p2
+                    select_p2 = p2_props_defined and not p_props_defined or p_is_lower_case and not p2_is_lower_case
+
+                    # one is more defined and is all lower, but the other is not all lower
+                    # (we assume the not all lower case is the correct naming)
+                    error_props_lower = (
+                        p_props_defined and p_is_lower_case and not p2_props_defined and not p2_is_lower_case
+                    )
+                    error_props_lower2 = (
+                        p2_props_defined and p2_is_lower_case and not p_props_defined and not p_is_lower_case
+                    )
+
+                    # both props are defined
+                    error_props = p_props_defined and p2_props_defined
+
+                    # the sources are different
+                    error_source = p.repository_host != p2.repository_host
+
+                    if error_props_lower or error_props_lower2 or error_props or error_source:
+                        self.line(" • <error>Cannot determine which is the correct plugin</error>")
+                        self.line(f" - {p.line}")
+                        self.line(f" - {p2.line}")
+                        error = True
+                        # remove second spec to not encounter the error twice
+                        # this will not be written to the manifest.txt because we set
+                        # the error flag and will exit after the loop
+                        specs.remove(p2)
+                    elif select_p:
+                        self.line(f" - <comment>{p.line}</comment>")
+                        self.line(f" - {p2.line}")
+                        specs.remove(p2)
+                    elif select_p2:
+                        self.line(f" - {p.line}")
+                        self.line(f" - <comment>{p2.line}</comment>")
+                        specs.remove(p)
+                    else:
+                        self.line(" • <error>Logic error in correct spec determination</error>")
+                        self.line(f" - {p.line}")
+                        self.line(f" - {p2.line}")
+                        error = True
+                        # remove second spec to not encounter the error twice
+                        # this will not be written to the manifest.txt because we set
+                        # the error flag and will exit after the loop
+                        specs.remove(p)
+        if error:
+            # exit after all errors have been found
+            exit(1)
+
+        return specs
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/helpers.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/helpers.py
new file mode 100644
index 00000000..8a28b0e8
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/helpers.py
@@ -0,0 +1,61 @@
+from .spec import PluginSpec
+
+MANIFEST_FILE = "manifest.txt"
+BLACKLIST_FILE = "blacklist.txt"
+PKGS_FILE = "default.nix"
+JSON_FILE = ".plugins.json"
+PLUGINS_LIST_FILE = "plugins.md"
+
+
+def get_const(const: str, plug_dir: str) -> str:
+    out = plug_dir + "/" + const
+    return out
+
+
+def read_manifest(plug_dir: str) -> list[str]:
+    with open(get_const(MANIFEST_FILE, plug_dir), "r") as file:
+        specs = set([spec.strip() for spec in file.readlines()])
+
+    return sorted(specs)
+
+
+def read_manifest_to_spec(plug_dir: str) -> list[PluginSpec]:
+    manifest = read_manifest(plug_dir)
+    specs = [PluginSpec.from_spec(spec.strip()) for spec in manifest]
+
+    return sorted(specs)
+
+
+def read_blacklist(plug_dir: str) -> list[str]:
+    with open(get_const(BLACKLIST_FILE, plug_dir), "r") as file:
+        if len(file.readlines()) == 0:
+            return [""]
+        else:
+            blacklisted_specs = set([spec.strip() for spec in file.readlines()])
+
+    return sorted(blacklisted_specs)
+
+
+def read_blacklist_to_spec(plug_dir: str) -> list[PluginSpec]:
+    blacklist = read_blacklist(plug_dir)
+    specs = [PluginSpec.from_spec(spec.strip()) for spec in blacklist]
+
+    return sorted(specs)
+
+
+def write_manifest(specs: list[str] | set[str], plug_dir: str):
+    """write specs to manifest file. Does some cleaning up"""
+
+    with open(get_const(MANIFEST_FILE, plug_dir), "w") as file:
+        specs = sorted(set(specs), key=lambda x: x.lower())
+        specs = [p for p in specs]
+
+        for s in specs:
+            file.write(f"{s}\n")
+
+
+def write_manifest_from_spec(specs: list[PluginSpec], plug_dir: str):
+    """write specs to manifest file. Does some cleaning up"""
+
+    strings = [f"{spec}" for spec in specs]
+    write_manifest(strings, plug_dir)
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/nix.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/nix.py
new file mode 100644
index 00000000..66a8df4c
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/nix.py
@@ -0,0 +1,121 @@
+import abc
+import enum
+import json
+import subprocess
+
+
+def nix_prefetch_url(url):
+    """Return the sha256 hash of the given url."""
+    subprocess_output = subprocess.check_output(
+        ["nix-prefetch-url", "--type", "sha256", url],
+        stderr=subprocess.DEVNULL,
+    )
+    sha256 = subprocess_output.decode("utf-8").strip()
+    return sha256
+
+
+def nix_prefetch_git(url):
+    """Return the sha256 hash of the given git url."""
+    subprocess_output = subprocess.check_output(["nix-prefetch-git", url], stderr=subprocess.DEVNULL)
+    sha256 = json.loads(subprocess_output)["sha256"]
+    return sha256
+
+
+class Source(abc.ABC):
+    """Abstract base class for sources."""
+
+    url: str
+    sha256: str
+
+    @abc.abstractmethod
+    def __init__(self, url: str) -> None:
+        """Initialize a Source."""
+
+    @abc.abstractmethod
+    def get_nix_expression(self):
+        """Return the nix expression for this source."""
+
+    def __repr__(self):
+        """Return the representation of this source."""
+        return self.get_nix_expression()
+
+
+class UrlSource(Source):
+    """A source that is a url."""
+
+    def __init__(self, url: str) -> None:
+        """Initialize a UrlSource."""
+        self.url = url
+        self.sha256 = nix_prefetch_url(url)
+
+    def get_nix_expression(self):
+        """Return the nix expression for this source."""
+        return f'fetchurl {{ url = "{self.url}"; sha256 = "{self.sha256}"; }}'
+
+
+class GitSource(Source):
+    """A source that is a git repository."""
+
+    def __init__(self, url: str, rev: str) -> None:
+        """Initialize a GitSource."""
+        self.url = url
+        self.rev = rev
+        self.sha256 = nix_prefetch_git(url)
+
+    def get_nix_expression(self):
+        """Return the nix expression for this source."""
+        return f'fetchgit {{ url = "{self.url}"; rev = "{self.rev}"; sha256 = "{self.sha256}"; }}'
+
+
+class License(enum.Enum):
+    """An enumeration of licenses."""
+
+    AGPL_3_0 = "agpl3Only"
+    APACHE_2_0 = "asf20"
+    BSD_2_CLAUSE = "bsd2"
+    BSD_3_CLAUSE = "bsd3"
+    BSL_1_0 = "bsl1_0"
+    CC0_1_0 = "cc0"
+    EPL_2_0 = "epl20"
+    GPL_2_0 = "gpl2Only"
+    GPL_3_0 = "gpl3Only"
+    ISCLGPL_2_1 = "lgpl21Only"
+    MIT = "mit"
+    MPL_2_0 = "mpl20"
+    UNLUNLICENSE = "unlicense"
+    WTFPL = "wtfpl"
+    UNFREE = "unfree"
+    UNKNOWN = ""
+
+    @classmethod
+    def from_spdx_id(cls, spdx_id: str | None) -> "License":
+        """Return the License from the given spdx_id."""
+        mapping = {
+            "AGPL-3.0": cls.AGPL_3_0,
+            "AGPL-3.0-only": cls.AGPL_3_0,
+            "Apache-2.0": cls.APACHE_2_0,
+            "BSD-2-Clause": cls.BSD_2_CLAUSE,
+            "BSD-3-Clause": cls.BSD_3_CLAUSE,
+            "BSL-1.0": cls.BSL_1_0,
+            "CC0-1.0": cls.CC0_1_0,
+            "EPL-2.0": cls.EPL_2_0,
+            "GPL-2.0": cls.GPL_2_0,
+            "GPL-2.0-only": cls.GPL_2_0,
+            "GPL-3.0": cls.GPL_3_0,
+            "GPL-3.0-only": cls.GPL_3_0,
+            "LGPL-2.1-only": cls.ISCLGPL_2_1,
+            "MIT": cls.MIT,
+            "MPL-2.0": cls.MPL_2_0,
+            "Unlicense": cls.UNLUNLICENSE,
+            "WTFPL": cls.WTFPL,
+        }
+
+        if spdx_id is None:
+            return cls.UNKNOWN
+
+        spdx_id = spdx_id.upper()
+        return mapping.get(spdx_id, cls.UNKNOWN)
+
+    def __str__(self):
+        """Return the string representation of this license."""
+        return self.value
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/plugin.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/plugin.py
new file mode 100644
index 00000000..8334ad53
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/plugin.py
@@ -0,0 +1,182 @@
+import logging
+import os
+import urllib
+
+import requests
+import jsonpickle
+from datetime import datetime, date
+from dateparser import parse
+
+from .nix import GitSource, License, Source, UrlSource
+from .spec import PluginSpec, RepositoryHost
+
+
+logger = logging.getLogger(__name__)
+
+
+class VimPlugin:
+    """Abstract base class for vim plugins."""
+
+    name: str
+    owner: str
+    repo: str
+    version: date
+    source: Source
+    description: str = "No description"
+    homepage: str
+    license: License
+    source_line: str
+    checked: date = datetime.now().date()
+
+    def to_nix(self):
+        """Return the nix expression for this plugin."""
+        meta = f'with lib; {{ description = "{self.description}"; homepage = "{self.homepage}"; license = with licenses; [ {self.license.value} ]; }}'
+        return f'/* Generated from: {self.source_line} */ {self.name} = buildVimPlugin {{ pname = "{self.name}";  version = "{self.version}"; src = {self.source.get_nix_expression()}; meta = {meta}; }};'
+
+    def to_json(self):
+        """Serizalize the plugin to json"""
+        return jsonpickle.encode(self)
+
+    def to_markdown(self):
+        link = f"[{self.source_line}]({self.homepage})"
+        version = f"{self.version}"
+        package_name = f"{self.name}"
+        checked = f"{self.checked}"
+
+        return f"| {link} | {version} | `{package_name}` | {checked} |"
+
+    def __lt__(self, o: object) -> bool:
+        if not isinstance(o, VimPlugin):
+            return False
+
+        return self.name.lower() < o.name.lower()
+
+    def __repr__(self):
+        """Return the representation of this plugin."""
+        return f"VimPlugin({self.name!r}, {self.version.strftime('%Y-%m-%d')})"
+
+
+def _get_github_token():
+    token = os.environ.get("GITHUB_TOKEN")
+    if token is None:
+        # NOTE: This should never use more than the free api requests <2023-12-09>
+        pass
+        # logger.warning("GITHUB_TOKEN environment variable not set")
+    return token
+
+
+class GitHubPlugin(VimPlugin):
+    def __init__(self, plugin_spec: PluginSpec) -> None:
+        """Initialize a GitHubPlugin."""
+
+        full_name = f"{plugin_spec.owner}/{plugin_spec.repo}"
+        repo_info = self._api_call(f"repos/{full_name}")
+        default_branch = plugin_spec.branch or repo_info["default_branch"]
+        api_callback = self._api_call(f"repos/{full_name}/commits/{default_branch}")
+        latest_commit = api_callback["commit"]
+        sha = api_callback["sha"]
+
+        self.name = plugin_spec.name
+        self.owner = plugin_spec.owner
+        self.version = parse(latest_commit["committer"]["date"]).date()
+        self.source = UrlSource(f"https://github.com/{full_name}/archive/{sha}.tar.gz")
+        self.description = (repo_info.get("description") or "").replace('"', '\\"')
+        self.homepage = repo_info["html_url"]
+        self.license = plugin_spec.license or License.from_spdx_id((repo_info.get("license") or {}).get("spdx_id"))
+        self.source_line = plugin_spec.line
+
+    def _api_call(self, path: str, token: str | None = _get_github_token()):
+        """Call the GitHub API."""
+        url = f"https://api.github.com/{path}"
+        headers = {"Content-Type": "application/json"}
+        if token is not None:
+            headers["Authorization"] = f"token {token}"
+        response = requests.get(url, headers=headers)
+        if response.status_code != 200:
+            raise RuntimeError(f"GitHub API call failed: {response.text}")
+        return response.json()
+
+
+class GitlabPlugin(VimPlugin):
+    def __init__(self, plugin_spec: PluginSpec) -> None:
+        """Initialize a GitlabPlugin."""
+
+        full_name = urllib.parse.quote(f"{plugin_spec.owner}/{plugin_spec.repo}", safe="")
+        repo_info = self._api_call(f"projects/{full_name}")
+        default_branch = plugin_spec.branch or repo_info["default_branch"]
+        api_callback = self._api_call(f"projects/{full_name}/repository/branches/{default_branch}")
+        latest_commit = api_callback["commit"]
+        sha = latest_commit["id"]
+
+        self.name = plugin_spec.name
+        self.owner = plugin_spec.owner
+        self.version = parse(latest_commit["created_at"]).date()
+        self.source = UrlSource(f"https://gitlab.com/api/v4/projects/{full_name}/repository/archive.tar.gz?sha={sha}")
+        self.description = (repo_info.get("description") or "").replace('"', '\\"')
+        self.homepage = repo_info["web_url"]
+        self.license = plugin_spec.license or License.from_spdx_id(repo_info.get("license", {}).get("key"))
+        self.source_line = plugin_spec.line
+
+    def _api_call(self, path: str) -> dict:
+        """Call the Gitlab API."""
+        url = f"https://gitlab.com/api/v4/{path}"
+        response = requests.get(url)
+        if response.status_code != 200:
+            raise RuntimeError(f"Gitlab API call failed: {response.text}")
+        return response.json()
+
+
+def _get_sourcehut_token():
+    token = os.environ.get("SOURCEHUT_TOKEN")
+    if token is None:
+        # NOTE: This should never use more than the free requests <2023-12-09>
+        pass
+        # logger.warning("SOURCEHUT_TOKEN environment variable not set")
+    return token
+
+
+class SourceHutPlugin(VimPlugin):
+    def __init__(self, plugin_spec: PluginSpec) -> None:
+        """Initialize a SourceHutPlugin."""
+
+        repo_info = self._api_call(f"~{plugin_spec.owner}/repos/{plugin_spec.repo}")
+        if plugin_spec.branch is None:
+            commits = self._api_call(f"~{plugin_spec.owner}/repos/{plugin_spec.repo}/log")
+        else:
+            commits = self._api_call(f"~{plugin_spec.owner}/repos/{plugin_spec.repo}/log/{plugin_spec.branch}")
+        latest_commit = commits["results"][0]
+        sha = latest_commit["id"]
+
+        self.name = plugin_spec.name
+        self.owner = plugin_spec.owner
+        self.version = parse(latest_commit["timestamp"]).date()
+        self.description = (repo_info.get("description") or "").replace('"', '\\"')
+        self.homepage = f"https://git.sr.ht/~{plugin_spec.owner}/{plugin_spec.repo}"
+        self.source = GitSource(self.homepage, sha)
+        self.license = plugin_spec.license or License.UNKNOWN  # cannot be determined via API
+        self.source_line = plugin_spec.line
+
+    def _api_call(self, path: str, token: str | None = _get_sourcehut_token()):
+        """Call the SourceHut API."""
+
+        url = f"https://git.sr.ht/api/{path}"
+        headers = {"Content-Type": "application/json"}
+        if token is not None:
+            headers["Authorization"] = f"token {token}"
+        response = requests.get(url, headers=headers)
+        if response.status_code != 200:
+            raise RuntimeError(f"SourceHut API call failed: {response.json()}")
+        return response.json()
+
+
+def plugin_from_spec(plugin_spec: PluginSpec) -> VimPlugin:
+    """Initialize a VimPlugin."""
+
+    if plugin_spec.repository_host == RepositoryHost.GITHUB:
+        return GitHubPlugin(plugin_spec)
+    elif plugin_spec.repository_host == RepositoryHost.GITLAB:
+        return GitlabPlugin(plugin_spec)
+    elif plugin_spec.repository_host == RepositoryHost.SOURCEHUT:
+        return SourceHutPlugin(plugin_spec)
+    else:
+        raise NotImplementedError(f"Unsupported source: {plugin_spec.repository_host}")
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/spec.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/spec.py
new file mode 100644
index 00000000..0f2fb29c
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/spec.py
@@ -0,0 +1,143 @@
+import enum
+import re
+
+from .nix import License
+
+
+class RepositoryHost(enum.Enum):
+    """A repository host."""
+
+    GITHUB = "github"
+    GITLAB = "gitlab"
+    SOURCEHUT = "sourcehut"
+
+
+class PluginSpec:
+    """A Vim plugin Spec."""
+
+    @classmethod
+    def from_spec(cls, spec):
+        """The spec line must be in the format:
+            [<repository_host>:]<owner>/<repo>[:<branch>][:name].
+
+        repository_host is one of github (default), gitlab, or sourcehut.
+        owner is the repository owner.
+        repo is the repository name.
+        branch is the git branch.
+        name is the name to use for the plugin (default is value of repo).
+        """
+        repository_host = RepositoryHost.GITHUB
+        # gitref = "master"
+
+        repository_host_regex = r"((?P<repository_host>[^:]+):)"
+        owner_regex = r"(?P<owner>[^/:]+)"
+        repo_regex = r"(?P<repo>[^:]+)"
+        branch_regex = r"(:(?P<branch>[^:]+)?)"
+        name_regex = r"(:(?P<name>[^:]+)?)"
+        license_regex = r"(:(?P<license>[^:]+)?)"
+        marked_duplicate_regex = r"(:(?P<duplicate>duplicate))"
+
+        spec_regex = re.compile(
+            f"^{repository_host_regex}?{owner_regex}/{repo_regex}{branch_regex}?{name_regex}?{license_regex}?{marked_duplicate_regex}?$",
+        )
+
+        match = spec_regex.match(spec)
+        if match is None:
+            raise ValueError(f"Invalid spec: {spec}")
+
+        group_dict = match.groupdict()
+
+        repository_host = RepositoryHost(group_dict.get("repository_host") or "github")
+
+        owner = group_dict.get("owner")
+        if owner is None:
+            raise RuntimeError("Could not get owner")
+
+        repo = group_dict.get("repo")
+        if repo is None:
+            raise RuntimeError("Could not get repo")
+
+        branch = group_dict.get("branch")
+        name = group_dict.get("name")
+        license = group_dict.get("license")
+        marked_duplicate = bool(group_dict.get("duplicate"))  # True if 'duplicate', False if None
+
+        line = spec
+
+        return cls(repository_host, owner, repo, line, branch, name, license, marked_duplicate)
+
+    def __init__(
+        self,
+        repository_host: RepositoryHost,
+        owner: str,
+        repo: str,
+        line: str,
+        branch: str | None = None,
+        name: str | None = None,
+        license: str | None = None,
+        marked_duplicate: bool = False,
+    ) -> None:
+        """Initialize a VimPluginSpec."""
+        self.repository_host = repository_host
+        self.owner = owner
+        self.repo = repo
+        self.branch = branch
+        self.custom_name = name
+        self.name = name or repo.replace(".", "-").replace("_", "-")
+        self.license = License(license) if license else None
+        self.line = line
+        self.marked_duplicate = marked_duplicate
+
+    def __str__(self) -> str:
+        """Return a string representation of a VimPluginSpec."""
+        spec = ""
+
+        if self.repository_host != RepositoryHost.GITHUB:
+            spec += f"{self.repository_host.value}:"
+
+        spec += f"{self.owner}/{self.repo}"
+
+        spec += ":"
+        if self.branch is not None:
+            spec += self.branch
+
+        spec += ":"
+        if self.custom_name is not None:
+            spec += self.custom_name
+
+        spec += ":"
+        if self.license is not None:
+            spec += str(self.license)
+
+        spec += ":"
+        if self.marked_duplicate:
+            spec += "duplicate"
+
+        return spec.rstrip(":")
+
+    def __repr__(self):
+        """Return the representation of the specs"""
+        return f"PluginSpec({self.owner}/{self.repo}, {self.name})"
+
+    def to_spec(self):
+        """Return a spec line for a VimPluginSpec."""
+        return str(self)
+
+    def __lt__(self, o: object) -> bool:
+        if not isinstance(o, PluginSpec):
+            return False
+
+        return self.name.lower() < o.name.lower()
+
+    def __eq__(self, o: object) -> bool:
+        """Return True if the two specs are equal."""
+        if not isinstance(o, PluginSpec):
+            return False
+
+        return (
+            self.repository_host == o.repository_host
+            and self.owner == o.owner
+            and self.repo == o.repo
+            and self.branch == o.branch
+            and self.name == o.name
+        )
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/__init__.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/__init__.py
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/fixtures.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/fixtures.py
new file mode 100644
index 00000000..75dd251a
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/fixtures.py
@@ -0,0 +1,44 @@
+import json
+
+import pytest
+from pytest_mock import MockerFixture
+
+from update_vim_plugins.nix import GitSource, UrlSource
+
+
+@pytest.fixture()
+def url():
+    return "https://example.com"
+
+
+@pytest.fixture()
+def rev():
+    return "1234567890abcdef"
+
+
+@pytest.fixture()
+def sha256():
+    return "sha256-1234567890abcdef"
+
+
+@pytest.fixture()
+def url_source(mocker: MockerFixture, url: str, sha256: str):
+    mocker.patch("subprocess.check_output", return_value=bytes(sha256, "utf-8"))
+    return UrlSource(url)
+
+
+@pytest.fixture()
+def git_source(mocker: MockerFixture, url: str, rev: str, sha256: str):
+    return_value = {
+        "url": url,
+        "rev": rev,
+        "date": "1970-01-01T00:00:00+00:00",
+        "path": "",
+        "sha256": sha256,
+        "fetchLFS": False,
+        "fetchSubmodules": False,
+        "deepClone": False,
+        "leaveDotGit": False,
+    }
+    mocker.patch("subprocess.check_output", return_value=json.dumps(return_value).encode("utf-8"))
+    return GitSource(url, rev)
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_nix.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_nix.py
new file mode 100644
index 00000000..46e59f76
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_nix.py
@@ -0,0 +1,32 @@
+from update_vim_plugins.nix import GitSource, License, UrlSource
+
+
+def test_url_source(url_source: UrlSource, url: str, sha256: str):
+    assert url_source.url == url
+    assert url_source.sha256 == sha256
+
+
+def test_url_source_nix_expression(url_source: UrlSource, url: str, sha256: str):
+    assert url_source.get_nix_expression() == f'fetchurl {{ url = "{url}"; sha256 = "{sha256}"; }}'
+
+
+def test_git_source(git_source: GitSource, url: str, rev: str, sha256: str):
+    assert git_source.url == url
+    assert git_source.sha256 == sha256
+    assert git_source.rev == rev
+
+
+def test_git_source_nix_expression(git_source: GitSource, url: str, rev: str, sha256: str):
+    assert git_source.get_nix_expression() == f'fetchgit {{ url = "{url}"; rev = "{rev}"; sha256 = "{sha256}"; }}'
+
+
+def test_license_github():
+    github_license = "MIT"
+    license = License.from_spdx_id(github_license)
+    assert license == License.MIT
+
+
+def test_license_gitlab():
+    gitlab_license = "mit"
+    license = License.from_spdx_id(gitlab_license)
+    assert license == License.MIT
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_plugin.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_plugin.py
new file mode 100644
index 00000000..32377e24
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_plugin.py
@@ -0,0 +1,144 @@
+import json
+from typing import Callable
+
+import pytest
+from pytest_mock import MockFixture
+
+from update_vim_plugins.nix import License, UrlSource
+from update_vim_plugins.plugin import GitHubPlugin, VimPlugin
+from update_vim_plugins.spec import PluginSpec
+
+
+@pytest.fixture()
+def mock_source(sha256: str):
+    class MockSource:
+        def __init__(self, *args, **kwargs):
+            pass
+
+        def get_nix_expression(self):
+            return "src"
+
+    return MockSource()
+
+
+@pytest.fixture()
+def mock_plugin(mock_source):
+    class MockVimPlugin(VimPlugin):
+        def __init__(self):
+            self.name = "test"
+            self.version = "1.0.0"
+            self.source = mock_source
+            self.description = "No description"
+            self.homepage = "https://example.com"
+            self.license = License.UNKNOWN
+
+    return MockVimPlugin()
+
+
+def test_vim_plugin_nix_expression(mock_plugin):
+    assert (
+        mock_plugin.get_nix_expression()
+        == 'test = buildVimPluginFrom2Nix { pname = "test"; version = "1.0.0"; src = src; meta = with lib; { description = "No description"; homepage = "https://example.com"; license = with licenses; [  ]; }; };'
+    )
+
+
+class MockResponse:
+    def __init__(self, status_code: int, content: bytes):
+        self.status_code = status_code
+        self.content = content
+
+    def json(self):
+        return json.loads(self.content)
+
+
+def mock_request_get(repsonses: dict[str, MockResponse]):
+    respones_not_found = MockResponse(404, b'{"message": "Not Found"}')
+
+    def mock_get(url: str, *args, **kwargs):
+        return repsonses.get(url, respones_not_found)
+
+    return mock_get
+
+
+@pytest.fixture()
+def github_commits_response():
+    return MockResponse(
+        200,
+        json.dumps(
+            {
+                "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
+                "commit": {
+                    "committer": {
+                        "date": "2011-04-14T16:00:49Z",
+                    },
+                },
+            }
+        ),
+    )
+
+
+@pytest.fixture()
+def github_get(github_commits_response: MockResponse):
+    repos_response = MockResponse(
+        200,
+        json.dumps(
+            {
+                "html_url": "https://github.com/octocat/Hello-World",
+                "description": "This your first repo!",
+                "fork": False,
+                "default_branch": "master",
+                "license": {
+                    "spdx_id": "MIT",
+                },
+            }
+        ),
+    )
+    responses = {
+        "https://api.github.com/repos/octocat/Hello-World": repos_response,
+        "https://api.github.com/repos/octocat/Hello-World/commits/master": github_commits_response,
+    }
+    return mock_request_get(responses)
+
+
+@pytest.fixture()
+def github_get_no_license(github_commits_response: MockResponse):
+    repos_response = MockResponse(
+        200,
+        json.dumps(
+            {
+                "html_url": "https://github.com/octocat/Hello-World",
+                "description": "This your first repo!",
+                "fork": False,
+                "default_branch": "master",
+            }
+        ),
+    )
+    responses = {
+        "https://api.github.com/repos/octocat/Hello-World": repos_response,
+        "https://api.github.com/repos/octocat/Hello-World/commits/master": github_commits_response,
+    }
+    return mock_request_get(responses)
+
+
+def test_github_plugin(mocker: MockFixture, github_get: Callable, url_source: UrlSource):
+    mocker.patch("requests.get", github_get)
+    url_source = mocker.patch("update_vim_plugins.nix.UrlSource", url_source)
+
+    spec = PluginSpec.from_spec("octocat/Hello-World")
+    plugin = GitHubPlugin(spec)
+
+    assert plugin.name == "Hello-World"
+    assert plugin.version == "2011-04-14"
+    assert plugin.description == "This your first repo!"
+    assert plugin.homepage == "https://github.com/octocat/Hello-World"
+    assert plugin.license == License.MIT
+
+
+def test_github_plugin_no_license(mocker: MockFixture, github_get_no_license: Callable, url_source: UrlSource):
+    mocker.patch("requests.get", github_get_no_license)
+    url_source = mocker.patch("update_vim_plugins.nix.UrlSource", url_source)
+
+    spec = PluginSpec.from_spec("octocat/Hello-World")
+    plugin = GitHubPlugin(spec)
+
+    assert plugin.license == License.UNKNOWN
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_spec.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_spec.py
new file mode 100644
index 00000000..2b9a1d24
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/tests/test_spec.py
@@ -0,0 +1,136 @@
+import pytest
+
+from update_vim_plugins.spec import PluginSpec, RepositoryHost
+
+
+@pytest.fixture()
+def owner():
+    return "owner"
+
+
+@pytest.fixture()
+def repo():
+    return "repo.nvim"
+
+
+@pytest.fixture()
+def branch():
+    return "main"
+
+
+@pytest.fixture()
+def name():
+    return "repo-nvim"
+
+
+@pytest.fixture()
+def license():
+    return "mit"
+
+
+def test_from_spec_simple(owner: str, repo: str):
+    vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}")
+
+    assert vim_plugin.owner == owner
+    assert vim_plugin.repo == repo
+
+
+def test_from_spec_with_gitref(owner: str, repo: str, branch: str):
+    vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}:{branch}")
+
+    assert vim_plugin.branch == branch
+
+
+def test_from_spec_with_name(owner: str, repo: str, name: str):
+    vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}::{name}")
+
+    assert vim_plugin.name == name
+
+
+@pytest.mark.parametrize("host", RepositoryHost)
+def test_from_spec_with_repository_host(owner: str, repo: str, host: RepositoryHost):
+    vim_plugin = PluginSpec.from_spec(f"{host.value}:{owner}/{repo}")
+
+    assert vim_plugin.repository_host == host
+
+
+def test_from_spec_without_repository_host(owner: str, repo: str):
+    vim_plugin = PluginSpec.from_spec(f"{owner}/{repo}")
+
+    assert vim_plugin.repository_host == RepositoryHost.GITHUB
+
+
+def test_from_spec_complex(owner: str, repo: str, branch: str, name: str):
+    vim_plugin = PluginSpec.from_spec(f"gitlab:{owner}/{repo}:{branch}:{name}")
+
+    assert vim_plugin.repository_host == RepositoryHost.GITLAB
+    assert vim_plugin.owner == owner
+    assert vim_plugin.repo == repo
+    assert vim_plugin.branch == branch
+    assert vim_plugin.name == name
+
+
+def test_from_spec_invalid_spec():
+    with pytest.raises(ValueError):
+        PluginSpec.from_spec("invalid_spec")
+
+
+def test_to_spec_simple(owner: str, repo: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+
+    assert vim_plugin.to_spec() == f"{owner}/{repo}"
+
+
+def test_to_spec_with_branch(owner: str, repo: str, branch: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo, branch=branch)
+    assert vim_plugin.to_spec() == f"{owner}/{repo}:{branch}"
+
+
+def test_to_spec_with_name(owner: str, repo: str, name: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo, name=name)
+
+    assert vim_plugin.to_spec() == f"{owner}/{repo}::{name}"
+
+
+@pytest.mark.parametrize("host", [RepositoryHost.GITLAB, RepositoryHost.SOURCEHUT])
+def test_to_spec_with_repository_host(host: RepositoryHost, owner: str, repo: str):
+    vim_plugin = PluginSpec(host, owner, repo)
+
+    assert vim_plugin.to_spec() == f"{host.value}:{owner}/{repo}"
+
+
+def test_to_spec_complex(owner: str, repo: str, branch: str, name: str, license: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITLAB, owner, repo, branch=branch, name=name, license=license)
+
+    assert vim_plugin.to_spec() == f"gitlab:{owner}/{repo}:{branch}:{name}:{license}"
+
+
+def test_spec_equal(owner: str, repo: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+    vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+
+    assert vim_plugin == vim_plugin2
+
+
+def test_spec_not_equal_different_branch(owner: str, repo: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+    vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo, branch="main")
+
+    assert vim_plugin != vim_plugin2
+
+
+def test_spec_not_equal_different_name(owner: str, repo: str):
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+    vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo, name="renamed")
+
+    assert vim_plugin != vim_plugin2
+
+
+def test_spec_equal_same_normalized_name(owner: str):
+    repo = "repo.nvim"
+    name = "repo-nvim"
+
+    vim_plugin = PluginSpec(RepositoryHost.GITHUB, owner, repo)
+    vim_plugin2 = PluginSpec(RepositoryHost.GITHUB, owner, repo, name=name)
+
+    assert vim_plugin == vim_plugin2
diff --git a/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/update.py b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/update.py
new file mode 100644
index 00000000..7eb3eeb4
--- /dev/null
+++ b/pkgs/by-name/up/update-vim-plugins/update_vim_plugins/update.py
@@ -0,0 +1,212 @@
+import subprocess
+from random import shuffle
+from cleo.helpers import argument, option
+from cleo.commands.command import Command
+from concurrent.futures import ThreadPoolExecutor, as_completed
+
+from pprint import pprint
+
+from .plugin import plugin_from_spec
+
+from .helpers import read_manifest_to_spec, get_const
+from .helpers import JSON_FILE, PLUGINS_LIST_FILE, PKGS_FILE
+
+import json
+import jsonpickle
+
+jsonpickle.set_encoder_options("json", sort_keys=True)
+
+
+class UpdateCommand(Command):
+    name = "update"
+    description = "Generate nix module from input file"
+    arguments = [argument("plug_dir", description="Path to the plugin directory", optional=False)]
+    options = [
+        option("all", "a", description="Update all plugins. Else only update new plugins", flag=True),
+        option("dry-run", "d", description="Show which plugins would be updated", flag=True),
+    ]
+
+    def handle(self):
+        """Main command function"""
+
+        plug_dir = self.argument("plug_dir")
+        self.specs = read_manifest_to_spec(plug_dir)
+
+        if self.option("all"):
+            # update all plugins
+            spec_list = self.specs
+            known_plugins = []
+        else:
+            # filter plugins we already know
+            spec_list = self.specs
+
+            with open(get_const(JSON_FILE, plug_dir), "r") as json_file:
+                data = json.load(json_file)
+
+                known_specs = list(filter(lambda x: x.line in data, spec_list))
+                known_plugins = [jsonpickle.decode(data[x.line]) for x in known_specs]
+
+                spec_list = list(filter(lambda x: x.line not in data, spec_list))
+
+        if self.option("dry-run"):
+            self.line("<comment>These plugins would be updated</comment>")
+            pprint(spec_list)
+            self.line(f"<info>Total:</info> {len(spec_list)}")
+            exit(0)
+
+        processed_plugins, failed_plugins, failed_but_known = self.process_manifest(spec_list, plug_dir)
+
+        processed_plugins += known_plugins  # add plugins from .plugins.json
+        processed_plugins: list = sorted(set(processed_plugins))  # remove duplicates based only on source line
+
+        self.check_duplicates(processed_plugins)
+
+        if failed_plugins != []:
+            self.line("<error>Not processed:</error> The following plugins could not be updated")
+            for s, e in failed_plugins:
+                self.line(f" - {s!r} - {e}")
+
+        if failed_but_known != []:
+            self.line(
+                "<error>Not updated:</error> The following plugins could not be updated but an older version is known"
+            )
+            for s, e in failed_but_known:
+                self.line(f" - {s!r} - {e}")
+
+        # update plugin "database"
+        self.write_plugins_json(processed_plugins, plug_dir)
+
+        # generate output
+        self.write_plugins_nix(processed_plugins, plug_dir)
+
+        self.write_plugins_markdown(processed_plugins, plug_dir)
+
+        self.line("<comment>Done</comment>")
+
+    def write_plugins_markdown(self, plugins, plug_dir):
+        """Write the list of all plugins to PLUGINS_LIST_FILE in markdown"""
+
+        plugins.sort()
+
+        self.line("<info>Updating plugins.md</info>")
+
+        header = f" - Plugin count: {len(plugins)}\n\n| Repo | Last Update | Nix package name | Last checked |\n|:---|:---|:---|:---|\n"
+
+        with open(get_const(PLUGINS_LIST_FILE, plug_dir), "w") as file:
+            file.write(header)
+            for plugin in plugins:
+                file.write(f"{plugin.to_markdown()}\n")
+
+    def write_plugins_nix(self, plugins, plug_dir):
+        self.line("<info>Generating nix output</info>")
+
+        plugins.sort()
+
+        header = "{ lib, buildVimPlugin, fetchurl, fetchgit }: {"
+        footer = "}"
+
+        with open(get_const(PKGS_FILE, plug_dir), "w") as file:
+            file.write(header)
+            for plugin in plugins:
+                file.write(f"{plugin.to_nix()}\n")
+            file.write(footer)
+
+        self.line("<info>Formatting nix output</info>")
+
+        subprocess.run(
+            ["alejandra", get_const(PKGS_FILE, plug_dir)],
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+        )
+
+    def write_plugins_json(self, plugins, plug_dir):
+        self.line("<info>Storing results in .plugins.json</info>")
+
+        plugins.sort()
+
+        with open(get_const(JSON_FILE, plug_dir), "r+") as json_file:
+            data = json.load(json_file)
+
+            for plugin in plugins:
+                data.update({f"{plugin.source_line}": plugin.to_json()})
+
+            json_file.seek(0)
+            json_file.write(json.dumps(data, indent=2, sort_keys=True))
+            json_file.truncate()
+
+    def check_duplicates(self, plugins):
+        """check for duplicates in proccesed_plugins"""
+        error = False
+        for i, plugin in enumerate(plugins):
+            for p in plugins[i + 1 :]:
+                if plugin.name == p.name:
+                    self.line(
+                        f"<error>Error:</error> The following two lines produce the same plugin name:\n - {plugin.source_line}\n - {p.source_line}\n -> {p.name}"
+                    )
+                    error = True
+
+        # We want to exit if the resulting nix file would be broken
+        # But we want to go through all plugins before we do so
+        if error:
+            exit(1)
+
+    def generate_plugin(self, spec, i, size, plug_dir):
+        debug_string = ""
+
+        processed_plugin = None
+        failed_but_known = None
+        failed_plugin = None
+        try:
+            debug_string += f" - <info>({i+1}/{size}) Processing</info> {spec!r}\n"
+            vim_plugin = plugin_from_spec(spec)
+            debug_string += f"   • <comment>Success</comment> {vim_plugin!r}\n"
+            processed_plugin = vim_plugin
+        except Exception as e:
+            debug_string += f"   • <error>Error:</error> Could not update <info>{spec.name}</info>. Keeping old values. Reason: {e}\n"
+            with open(get_const(JSON_FILE, plug_dir), "r") as json_file:
+                data = json.load(json_file)
+
+            plugin_json = data.get(spec.line)
+            if plugin_json:
+                vim_plugin = jsonpickle.decode(plugin_json)
+                processed_plugin = vim_plugin
+                failed_but_known = (vim_plugin, e)
+            else:
+                debug_string += f"   • <error>Error:</error> No entries for <info>{spec.name}</info> in '.plugins.json'. Skipping...\n"
+                failed_plugin = (spec, e)
+
+        self.line(debug_string.strip())
+
+        return processed_plugin, failed_plugin, failed_but_known
+
+    def process_manifest(self, spec_list, plug_dir):
+        """Read specs in 'spec_list' and generate plugins"""
+
+        size = len(spec_list)
+
+        # We have to assume that we will reach an api limit. Therefore
+        # we randomize the spec list to give every entry the same change to be updated and
+        # not favor those at the start of the list
+        shuffle(spec_list)
+
+        with ThreadPoolExecutor() as executor:
+            futures = [
+                executor.submit(self.generate_plugin, spec, i, size, plug_dir) for i, spec in enumerate(spec_list)
+            ]
+            results = [future.result() for future in as_completed(futures)]
+
+        processed_plugins = [r[0] for r in results]
+        failed_plugins = [r[1] for r in results]
+        failed_but_known = [r[2] for r in results]
+
+        processed_plugins = list(filter(lambda x: x is not None, processed_plugins))
+        failed_plugins = list(filter(lambda x: x is not None, failed_plugins))
+        failed_but_known = list(filter(lambda x: x is not None, failed_but_known))
+
+        processed_plugins.sort()
+        failed_plugins.sort()
+        failed_but_known.sort()
+
+        assert len(processed_plugins) == len(spec_list) - len(failed_plugins)
+
+        return processed_plugins, failed_plugins, failed_but_known