about summary refs log tree commit diff stats
path: root/sys/nixpkgs/pkgs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--sys/nixpkgs/pkgs/default.nix4
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/README.md94
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/check.nix37
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/default.nix15
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/overrides.nix251
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/plugins/.plugins.json6
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/plugins/blacklist.txt1
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/plugins/default.nix55
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/plugins/manifest.txt3
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/plugins/plugins.md7
-rw-r--r--sys/nixpkgs/pkgs/plgs-pkgs/plugins/whitelist.txt0
-rwxr-xr-xsys/nixpkgs/pkgs/plgs-pkgs/update_neovim_plugins26
-rw-r--r--sys/nixpkgs/pkgs/snap-sync-forked/default.nix4
-rwxr-xr-xsys/nixpkgs/pkgs/update_vim_plugins/check-duplicates.sh49
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/default.nix17
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/package.nix47
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/poetry.lock838
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/pyproject.toml45
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/__init__.py0
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/__main__.py15
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/cleanup.py100
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/helpers.py61
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/nix.py121
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/plugin.py182
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/spec.py143
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/__init__.py0
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/fixtures.py44
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_nix.py34
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_plugin.py146
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_spec.py136
-rw-r--r--sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/update.py212
31 files changed, 2690 insertions, 3 deletions
diff --git a/sys/nixpkgs/pkgs/default.nix b/sys/nixpkgs/pkgs/default.nix
index b057068c..8a0898f1 100644
--- a/sys/nixpkgs/pkgs/default.nix
+++ b/sys/nixpkgs/pkgs/default.nix
@@ -1,5 +1,7 @@
 {sysLib}: let
   snap-sync-forked = (import ./snap-sync-forked) {inherit sysLib;};
-  overlays = [] ++ snap-sync-forked;
+  nvim_plugs = import ./plgs-pkgs;
+  update_vim_plugins = import ./update_vim_plugins;
+  overlays = [] ++ snap-sync-forked ++ nvim_plugs ++ update_vim_plugins;
 in
   overlays
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/README.md b/sys/nixpkgs/pkgs/plgs-pkgs/README.md
new file mode 100644
index 00000000..ed05f4b2
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/README.md
@@ -0,0 +1,94 @@
+# Fork
+All files in this repository where forked form [here](https://github.com/NixNeovim/NixNeovimPlugins) on commit `5010b91eb03696574c3c293f072a090618227e87`.
+Below the original README. They were licensed under the MIT license.
+# All vim plugins, ready to go
+
+This repo auto generates nix packages for vim/neovim plugins.
+Packages are automatically updated twice per week using a GitHub Actions.
+Plugins are fetched from the `manifest.txt` and [awesome-neovim][0] repo.
+
+This is a fork of [this repo](https://github.com/m15a/nixpkgs-vim-extra-plugins); however, we fetch all additions from the original repo, so we will never have less plugins.
+Further, the original deletes plugins that are available in the nixpkgs. We, instead, try to assemble a list of all available plugins.
+Therefore, to access plugins you will never have to search in two places.
+
+This repo can be used as a stand-alone, by adding it to your inputs.
+However, we recommend to use [NixNeovim](https://github.com/NixNeovim/NixNeovim) modules instead, and use this only when you need a plugins, which does not have a module, yet.
+
+## Available plugins
+
+The [plugins.md](plugins.md) contains an auto-generated list of all available plugins.
+
+## Usage
+
+- We recommend using [NixNeovim](https://github.com/NixNeovim/NixNeovim), and only access the plugins directly when they do not have a module in NixNeovim.
+
+However, you can also use this repo without NixNeovim:
+To access the plugins, you need to add the overlay.
+The overlay adds extra Vim plugins to `pkgs.vimExtraPlugins`.
+First, add this repo to your inputs:
+
+```
+inputs.nixneovimplugins.url = github:jooooscha/nixpkgs-vim-extra-plugins
+```
+
+Next, apply the provided overlay:
+
+```
+nixpkgs.overlays = [
+  inputs.nixneovimplugins.overlays.default
+];
+```
+
+Finally, you can add the packages to your vim/neovim config. For example you can use [NixNeovim](https://github.com/NixNeovim/Nixneovim) or you can add the plugins directly:
+
+```
+ programs.neovim = {
+   plugins = [
+     pkgs.vimExtraPlugins.nvim-colorizer-lua
+   ];
+ }
+```
+
+More info on using neovim with nix can be found here: [NixOS Neovim](https://nixos.wiki/wiki/Neovim)
+
+[0]: https://github.com/rockerBOO/awesome-neovim
+[1]: https://nixos.org/manual/nix/stable/release-notes/rl-2.4.html?highlight=builtins.getFlake#other-features
+[2]: https://nur.nix-community.org/
+[3]: https://nur.nix-community.org/repos/m15a/
+
+
+## Contribution
+
+### How to add a new plugin
+
+#### 1. Add the plugin to manifest.txt:
+
+```
+# Examples
+
+haringsrob/nvim_context_vt
+sourcehut:henriquehbr/ataraxis.lua
+gitlab:yorickpeterse/nvim-pqf
+williamboman/mason.nvim:45b9a4da776d9fb017960b3ac7241161fb7bc578
+foo/bar::baz                   --> renamed to baz
+foo/bar:dev                    --> using dev branch
+```
+
+Supported are Github (default), SourceHut, and GitLab.
+
+#### 2. Create a Pull Request
+
+- Create a pull request with the changed manifest.txt (and blacklist.txt if neccessary).
+- A GitHub action will check your contribution and generate all neccessary nix code for your new plugin. It will also take care of sorting and cleaning the manifest.txt
+- After all checks have passed, I will merge your change.
+
+I am happy for any contribution. :)
+
+### How to remove a new plugin
+
+Copy the entry from manifest.txt to blacklist.txt and create a PR.
+The GitHub Actions will do the rest, including removing the entry from manifest.txt
+
+## Credits
+
+This is originally based on work by [m15a](https://github.com/m15a/nixpkgs-vim-extra-plugins)
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/check.nix b/sys/nixpkgs/pkgs/plgs-pkgs/check.nix
new file mode 100644
index 00000000..ad23e2c7
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/check.nix
@@ -0,0 +1,37 @@
+{
+  pkgs,
+  lib,
+  ...
+}: let
+  # checks if a plugin has a license
+  hasLicense = _: pkg: let
+    warn = x: lib.warn x x;
+
+    msg =
+      if builtins.hasAttr "license" pkg.meta
+      then "${pkg.name} has license"
+      else warn "${pkg.name} has no license";
+
+    msg' = lib.replaceStrings [" "] ["-"] msg;
+  in
+    pkgs.runCommandNoCC msg' {} "echo : > $out ";
+
+  # function to check License for all packages
+  check-missing-licenses = let
+    buildInputs =
+      lib.mapAttrsToList
+      hasLicense
+      pkgs.vimExtraPlugins;
+  in
+    pkgs.runCommandNoCC
+    "check-missing-licenses"
+    {inherit buildInputs;}
+    "echo : > $out";
+in {
+  checks =
+    pkgs.vimExtraPlugins
+    // {
+      inherit check-missing-licenses;
+      inherit (pkgs) update-vim-plugins;
+    };
+}
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/default.nix b/sys/nixpkgs/pkgs/plgs-pkgs/default.nix
new file mode 100644
index 00000000..0f7cd485
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/default.nix
@@ -0,0 +1,15 @@
+[
+  (final: prev:
+    prev.lib.composeManyExtensions [
+      (self: super: let
+        origin = import ./plugins {
+          inherit (super.vimUtils) buildVimPlugin;
+          inherit (super) lib fetchurl fetchgit;
+        };
+      in {
+        vimExtraPlugins = super.lib.makeExtensible (_: super.lib.recurseIntoAttrs origin);
+      })
+    ]
+    final
+    prev)
+]
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/overrides.nix b/sys/nixpkgs/pkgs/plgs-pkgs/overrides.nix
new file mode 100644
index 00000000..c4a32026
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/overrides.nix
@@ -0,0 +1,251 @@
+final: prev: let
+  inherit (final) lib;
+
+  /*
+  * Mark broken packages here.
+  */
+  markBrokenPackages = self: super:
+    lib.mapAttrs (attrName: broken:
+      super.${attrName}.overrideAttrs (old: {
+        meta = old.meta // {inherit broken;};
+      }))
+    {
+      go-nvim = true;
+
+      highlight-current-n-nvim = true;
+
+      snippet-converter-nvim = true;
+
+      vacuumline-nvim = true;
+
+      vgit-nvim = true;
+
+      unruly-worker = true;
+    };
+
+  /*
+  * Add licenses if missing or incorrect in generated ./pkgs/vim-plugins.nix.
+  */
+  fixLicenses = self: super:
+    lib.mapAttrs (attrName: license:
+      super.${attrName}.overrideAttrs (old: {
+        meta = old.meta // {inherit license;};
+      })) (with lib.licenses; {
+      ariake-vim-colors = [mit];
+
+      bats-vim = [vim];
+
+      christmas-vim = [mit];
+
+      coc-tailwind-intellisense = [mit];
+
+      distant-nvim = [asl20 mit];
+
+      goimpl-nvim = [mit];
+
+      null-ls-nvim = [publicDomain];
+
+      nvim-base16-lua = [mit];
+
+      nvim-cartographer = [gpl3Plus];
+
+      nvim-deus = [gpl3Plus];
+
+      nvim-pqf = [mpl20];
+
+      nvim-remote-containers = [
+        rec {
+          shortName = fullName;
+          fullName = "jamestthompson3's modified MIT License";
+          url = "https://github.com/jamestthompson3/nvim-remote-containers/blob/master/LICENSE";
+          free = true;
+          redistributable = true;
+          deprecated = false;
+        }
+      ];
+
+      nvim-revJ-lua = [vim];
+
+      nvim-srcerite = [gpl3Plus];
+
+      nvim-window = [mpl20];
+
+      osc-nvim = [mit];
+
+      vim-emacscommandline = [vim];
+
+      vim-hy = [vim];
+
+      vim-textobj-indent = [mit];
+
+      vim-textobj-parameter = [mit];
+    });
+
+  # /*
+  #  * Add dependencies to vim plugins if missing or incorrect in generated ./pkgs/vim-plugins.nix.
+  #  */
+  # fixDependencies = self: super:
+  # lib.mapAttrs (attrName: dependencies: super.${attrName}.overrideAttrs (_: {
+  #   inherit dependencies;
+  # })) (with final.vimPlugins;
+  # {
+  #   apprentice-nvim = [ lush-nvim ];
+
+  #   auto-pandoc-nvim = [ plenary-nvim ];
+
+  #   code-runner-nvim = [ plenary-nvim ];
+
+  #   codeschool-nvim = [ lush-nvim ];
+
+  #   express-line-nvim = [ plenary-nvim ];
+
+  #   follow-md-links-nvim = [ nvim-treesitter ];
+
+  #   fuzzy-nvim = [ plenary-nvim ];
+
+  #   github-colors = [ nvim-treesitter ];
+
+  #   gloombuddy = [ colorbuddy-nvim ];
+
+  #   go-nvim = [ nvim-treesitter ];
+
+  #   goimpl-nvim = [ nvim-treesitter telescope-nvim ];
+
+  #   gruvbuddy-nvim = [ colorbuddy-nvim ];
+
+  #   gruvy = [ lush-nvim ];
+
+  #   jester = [ nvim-treesitter ];
+
+  #   lspactions = [ plenary-nvim popup-nvim ];
+
+  #   lspactions-nvim06-compatible = [ plenary-nvim popup-nvim self.astronauta-nvim ];
+
+  #   navigator-lua = [ nvim-lspconfig self.guihua-lua ];
+
+  #   neogen = [ nvim-treesitter ];
+
+  #   nlsp-settings-nvim = [ nvim-lspconfig ];
+
+  #   nvim-comment-frame = [ nvim-treesitter ];
+
+  #   nvim-go = [ plenary-nvim popup-nvim ];
+
+  #   nvim-lsp-basics = [ nvim-lspconfig ];
+
+  #   nvim-lspfuzzy = [ fzfWrapper ];
+
+  #   nvim-lsp-installer = [ nvim-lspconfig ];
+
+  #   nvim-lspupdate = [ nvim-lspconfig ];
+
+  #   nvim-magic = [ plenary-nvim nui-nvim ];
+
+  #   nvim-rdark = [ colorbuddy-nvim ];
+
+  #   nvim-revJ-lua = [ self.vim-textobj-parameter ];
+
+  #   nvim-spectre = [ plenary-nvim ];
+
+  #   nvim-treesitter-textsubjects = [ nvim-treesitter ];
+
+  #   nvim-treehopper = [ nvim-treesitter ];
+
+  #   nvim-ts-context-commentstring = [ nvim-treesitter ];
+
+  #   one-small-step-for-vimkind = [ nvim-dap ];
+
+  #   onebuddy = [ colorbuddy-nvim ];
+
+  #   reaper-nvim = [ self.osc-nvim ];
+
+  #   sqls-nvim = [ nvim-lspconfig ];
+
+  #   startup-nvim = [ telescope-nvim ];
+
+  #   tabline-framework-nvim = [ nvim-web-devicons ];
+
+  #   tabout-nvim = [ nvim-treesitter ];
+
+  #   telescope-bibtex-nvim = [ telescope-nvim ];
+
+  #   telescope-command-palette-nvim = [ telescope-nvim ];
+
+  #   telescope-heading-nvim = [ telescope-nvim ];
+
+  #   telescope-tmuxinator-nvim = [ telescope-nvim ];
+
+  #   vacuumline-nvim = [ galaxyline-nvim ];
+
+  #   vgit-nvim = [ plenary-nvim ];
+
+  #   vim-textobj-parameter = [ vim-textobj-user ];
+
+  #   virtual-types-nvim = [ nvim-lspconfig ];
+
+  #   yabs-nvim = [ plenary-nvim ];
+
+  #   zenbones-nvim = [ lush-nvim ];
+  # });
+
+  /*
+  * Add other overrides here.
+  */
+  otherOverrides = self: super: {
+    feline-nvim = super.feline-nvim.overrideAttrs (old: {
+      patches =
+        (old.patches or [])
+        ++ lib.optionals (lib.versionOlder old.version "2021-12-19") [
+          # https://github.com/famiu/feline.nvim/pull/179
+          (final.fetchpatch {
+            url = "https://github.com/zbirenbaum/feline.nvim/commit/d62d9ec923fe76da27f5ac7000b2a506b035740d.patch";
+            sha256 = "sha256-fLa6za0Srm/gnVPlPgs11+2cxhj7hitgUhlHu2jc2+s=";
+          })
+        ];
+    });
+
+    glow-nvim = super.glow-nvim.overrideAttrs (old: {
+      patches =
+        (old.patches or [])
+        ++ [
+          # https://github.com/ellisonleao/glow.nvim/pull/80
+          (final.fetchpatch {
+            url = "https://github.com/ellisonleao/glow.nvim/pull/80/commits/16a348ffa8022945f735caf708c2bd601b08272c.patch";
+            sha256 = "sha256-fljlBTHcoPjnavF37yoIs3zrZ3+iOX6oQ0e20AKtNI8=";
+          })
+        ];
+    });
+
+    guihua-lua = super.guihua-lua.overrideAttrs (old: {
+      buildPhase = ''
+        (
+          cd lua/fzy
+          make
+        )
+      '';
+    });
+
+    mdeval-nvim = super.mdeval-nvim.overrideAttrs (old: {
+      postPatch =
+        (old.postPatch or "")
+        + ''
+          sed -Ei lua/mdeval.lua \
+              -e 's@(get_command\(string\.format\(")mkdir@\1${final.coreutils}/bin/mkdir@' \
+              -e 's@(get_command\(string\.format\(")rm@\1${final.coreutils}/bin/rm@' \
+              -e 's@(2>&1; )echo@\1${final.coreutils}/bin/echo@'
+        '';
+    });
+
+    nvim-papadark = self.themer-lua;
+
+    feline-nvim-develop = self.feline-nvim;
+  };
+in {
+  vimExtraPlugins = prev.vimExtraPlugins.extend (lib.composeManyExtensions [
+    markBrokenPackages
+    fixLicenses
+    fixDependencies
+    # onceHereButNowOfficiallyMaintainedPlugins
+    otherOverrides
+  ]);
+}
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/plugins/.plugins.json b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/.plugins.json
new file mode 100644
index 00000000..46df05f9
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/.plugins.json
@@ -0,0 +1,6 @@
+{
+  "akinsho/toggleterm.nvim": "{\"description\": \"A neovim lua plugin to help easily manage multiple terminal windows\", \"homepage\": \"https://github.com/akinsho/toggleterm.nvim\", \"license\": {\"py/reduce\": [{\"py/type\": \"update_vim_plugins.nix.License\"}, {\"py/tuple\": [\"gpl3Only\"]}]}, \"name\": \"toggleterm-nvim\", \"owner\": \"akinsho\", \"py/object\": \"update_vim_plugins.plugin.GitHubPlugin\", \"source\": {\"py/object\": \"update_vim_plugins.nix.UrlSource\", \"sha256\": \"0nx69q9597vy7lzvvh58fnjyin23ns6apmyp532sgf547bw7mld6\", \"url\": \"https://github.com/akinsho/toggleterm.nvim/archive/cbd041d91b90cd3c02df03fe6133208888f8e008.tar.gz\"}, \"source_line\": \"akinsho/toggleterm.nvim\", \"version\": {\"__reduce__\": [{\"py/type\": \"datetime.date\"}, [\"B+cMBg==\"]], \"py/object\": \"datetime.date\"}}",
+  "andrewferrier/debugprint.nvim": "{\"description\": \"Debugging in NeoVim the print() way!\", \"homepage\": \"https://github.com/andrewferrier/debugprint.nvim\", \"license\": {\"py/reduce\": [{\"py/type\": \"update_vim_plugins.nix.License\"}, {\"py/tuple\": [\"\"]}]}, \"name\": \"debugprint-nvim\", \"owner\": \"andrewferrier\", \"py/object\": \"update_vim_plugins.plugin.GitHubPlugin\", \"source\": {\"py/object\": \"update_vim_plugins.nix.UrlSource\", \"sha256\": \"12xp4ziray4piynjgwbgdbylday20sdfxslq24h0c3ihdy8wf7ij\", \"url\": \"https://github.com/andrewferrier/debugprint.nvim/archive/8a6d66bd6162e9c49804e9286a7d4ceba60355d5.tar.gz\"}, \"source_line\": \"andrewferrier/debugprint.nvim\", \"version\": {\"__reduce__\": [{\"py/type\": \"datetime.date\"}, [\"B+cLHA==\"]], \"py/object\": \"datetime.date\"}}",
+  "lmburns/lf.nvim": "{\"description\": \"Lf file manager for Neovim (in Lua)\", \"homepage\": \"https://github.com/lmburns/lf.nvim\", \"license\": {\"py/reduce\": [{\"py/type\": \"update_vim_plugins.nix.License\"}, {\"py/tuple\": [\"mit\"]}]}, \"name\": \"lf-nvim\", \"owner\": \"lmburns\", \"py/object\": \"update_vim_plugins.plugin.GitHubPlugin\", \"source\": {\"py/object\": \"update_vim_plugins.nix.UrlSource\", \"sha256\": \"1nwf90bnzqhlgs007gg6xpx0vf4r1d19586nld78ipi1ch7nz4px\", \"url\": \"https://github.com/lmburns/lf.nvim/archive/69ab1efcffee6928bf68ac9bd0c016464d9b2c8b.tar.gz\"}, \"source_line\": \"lmburns/lf.nvim\", \"version\": {\"__reduce__\": [{\"py/type\": \"datetime.date\"}, [\"B+cKAw==\"]], \"py/object\": \"datetime.date\"}}",
+  "nvim-telescope/telescope-bibtex.nvim": "{\"description\": \"A telescope.nvim extension to search and paste bibtex entries into your TeX files.\", \"homepage\": \"https://github.com/nvim-telescope/telescope-bibtex.nvim\", \"license\": {\"py/reduce\": [{\"py/type\": \"update_vim_plugins.nix.License\"}, {\"py/tuple\": [\"mit\"]}]}, \"name\": \"telescope-bibtex-nvim\", \"owner\": \"nvim-telescope\", \"py/object\": \"update_vim_plugins.plugin.GitHubPlugin\", \"source\": {\"py/object\": \"update_vim_plugins.nix.UrlSource\", \"sha256\": \"1vllzdh9ammsfr76mg3brl540b3i6197v8bbgi0lj7s95qy9mj5y\", \"url\": \"https://github.com/nvim-telescope/telescope-bibtex.nvim/archive/b10ec78df938a1e06217f965b32fb1b960681cff.tar.gz\"}, \"source_line\": \"nvim-telescope/telescope-bibtex.nvim\", \"version\": {\"__reduce__\": [{\"py/type\": \"datetime.date\"}, [\"B+cLFA==\"]], \"py/object\": \"datetime.date\"}}"
+}
\ No newline at end of file
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/plugins/blacklist.txt b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/blacklist.txt
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/blacklist.txt
@@ -0,0 +1 @@
+
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/plugins/default.nix b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/default.nix
new file mode 100644
index 00000000..0d798d3e
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/default.nix
@@ -0,0 +1,55 @@
+{
+  lib,
+  buildVimPlugin,
+  fetchurl,
+  fetchgit,
+}: {
+  /*
+  Generated from: andrewferrier/debugprint.nvim
+  */
+  debugprint-nvim = buildVimPlugin {
+    pname = "debugprint-nvim";
+    version = "2023-11-28";
+    src = fetchurl {
+      url = "https://github.com/andrewferrier/debugprint.nvim/archive/8a6d66bd6162e9c49804e9286a7d4ceba60355d5.tar.gz";
+      sha256 = "12xp4ziray4piynjgwbgdbylday20sdfxslq24h0c3ihdy8wf7ij";
+    };
+    meta = with lib; {
+      description = "Debugging in NeoVim the print() way!";
+      homepage = "https://github.com/andrewferrier/debugprint.nvim";
+      license = with licenses; [];
+    };
+  };
+  /*
+  Generated from: lmburns/lf.nvim
+  */
+  lf-nvim = buildVimPlugin {
+    pname = "lf-nvim";
+    version = "2023-10-03";
+    src = fetchurl {
+      url = "https://github.com/lmburns/lf.nvim/archive/69ab1efcffee6928bf68ac9bd0c016464d9b2c8b.tar.gz";
+      sha256 = "1nwf90bnzqhlgs007gg6xpx0vf4r1d19586nld78ipi1ch7nz4px";
+    };
+    meta = with lib; {
+      description = "Lf file manager for Neovim (in Lua)";
+      homepage = "https://github.com/lmburns/lf.nvim";
+      license = with licenses; [mit];
+    };
+  };
+  /*
+  Generated from: nvim-telescope/telescope-bibtex.nvim
+  */
+  telescope-bibtex-nvim = buildVimPlugin {
+    pname = "telescope-bibtex-nvim";
+    version = "2023-11-20";
+    src = fetchurl {
+      url = "https://github.com/nvim-telescope/telescope-bibtex.nvim/archive/b10ec78df938a1e06217f965b32fb1b960681cff.tar.gz";
+      sha256 = "1vllzdh9ammsfr76mg3brl540b3i6197v8bbgi0lj7s95qy9mj5y";
+    };
+    meta = with lib; {
+      description = "A telescope.nvim extension to search and paste bibtex entries into your TeX files.";
+      homepage = "https://github.com/nvim-telescope/telescope-bibtex.nvim";
+      license = with licenses; [mit];
+    };
+  };
+}
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/plugins/manifest.txt b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/manifest.txt
new file mode 100644
index 00000000..86df059f
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/manifest.txt
@@ -0,0 +1,3 @@
+andrewferrier/debugprint.nvim
+lmburns/lf.nvim
+nvim-telescope/telescope-bibtex.nvim
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/plugins/plugins.md b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/plugins.md
new file mode 100644
index 00000000..73653272
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/plugins.md
@@ -0,0 +1,7 @@
+ - Plugin count: 3
+
+| Repo | Last Update | Nix package name | Last checked |
+|:---|:---|:---|:---|
+| [andrewferrier/debugprint.nvim](https://github.com/andrewferrier/debugprint.nvim) | 2023-11-28 | `debugprint-nvim` | 2023-12-09 |
+| [lmburns/lf.nvim](https://github.com/lmburns/lf.nvim) | 2023-10-03 | `lf-nvim` | 2023-12-09 |
+| [nvim-telescope/telescope-bibtex.nvim](https://github.com/nvim-telescope/telescope-bibtex.nvim) | 2023-11-20 | `telescope-bibtex-nvim` | 2023-12-09 |
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/plugins/whitelist.txt b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/whitelist.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/plugins/whitelist.txt
diff --git a/sys/nixpkgs/pkgs/plgs-pkgs/update_neovim_plugins b/sys/nixpkgs/pkgs/plgs-pkgs/update_neovim_plugins
new file mode 100755
index 00000000..ea71c6fa
--- /dev/null
+++ b/sys/nixpkgs/pkgs/plgs-pkgs/update_neovim_plugins
@@ -0,0 +1,26 @@
+#!/usr/bin/env sh
+
+BASE_DIR="$(readlink -f "$(dirname "$0")/plugins")"
+
+# Fetch plugins
+cd "$BASE_DIR" || (echo "BUG: No '$BASE_DIR'" && exit 1)
+
+# Cleanup manifest
+sort -o "$BASE_DIR/manifest.txt" "$BASE_DIR/manifest.txt"
+sort -o "$BASE_DIR/blacklist.txt" "$BASE_DIR/blacklist.txt"
+## Remove all plugins, which are on the blacklist
+echo "$(comm -23 "$BASE_DIR/manifest.txt" "$BASE_DIR/blacklist.txt")" > "$BASE_DIR/manifest.txt"
+
+# Backup vim-plugins.nix
+mv "$BASE_DIR/default.nix" "$BASE_DIR/default.nix.bak"
+echo "{...} : {}" > "$BASE_DIR/default.nix"
+
+# Generate derivations for new plugins (this binary is provided by the dev-environment)
+update-vim-plugins cleanup "$BASE_DIR"
+
+# Restore vim-plugins.nix
+mv "$BASE_DIR/default.nix.bak" "$BASE_DIR/default.nix"
+
+
+# Update new plugins
+update-vim-plugins update "$BASE_DIR" --all
diff --git a/sys/nixpkgs/pkgs/snap-sync-forked/default.nix b/sys/nixpkgs/pkgs/snap-sync-forked/default.nix
index 55e2bd27..f110a4fa 100644
--- a/sys/nixpkgs/pkgs/snap-sync-forked/default.nix
+++ b/sys/nixpkgs/pkgs/snap-sync-forked/default.nix
@@ -1,9 +1,9 @@
 {sysLib}: [
-  (self: super: {
+  (final: prev: {
     snap-sync-forked = sysLib.writeShellScriptWithLibrary {
       name = "snap-sync-forked";
       src = ./snap-sync-forked;
-      dependencies = with self; [
+      dependencies = with prev; [
         bash
         btrfs-progs
         coreutils
diff --git a/sys/nixpkgs/pkgs/update_vim_plugins/check-duplicates.sh b/sys/nixpkgs/pkgs/update_vim_plugins/check-duplicates.sh
new file mode 100755
index 00000000..dcf8b46f
--- /dev/null
+++ b/sys/nixpkgs/pkgs/update_vim_plugins/check-duplicates.sh
@@ -0,0 +1,49 @@
+#!/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 not add " " here. It would break the plugin
+        do
+            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/sys/nixpkgs/pkgs/update_vim_plugins/default.nix b/sys/nixpkgs/pkgs/update_vim_plugins/default.nix
new file mode 100644
index 00000000..7f0b3f0d
--- /dev/null
+++ b/sys/nixpkgs/pkgs/update_vim_plugins/default.nix
@@ -0,0 +1,17 @@
+[
+  (
+    final: prev: {
+      update-vim-plugins = import ./package.nix {
+        inherit
+          (prev)
+          python3
+          # dependencies
+          
+          nix
+          alejandra
+          nix-prefetch-git
+          ;
+      };
+    }
+  )
+]
diff --git a/sys/nixpkgs/pkgs/update_vim_plugins/package.nix b/sys/nixpkgs/pkgs/update_vim_plugins/package.nix
new file mode 100644
index 00000000..e74a29b1
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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/sys/nixpkgs/pkgs/update_vim_plugins/poetry.lock b/sys/nixpkgs/pkgs/update_vim_plugins/poetry.lock
new file mode 100644
index 00000000..8512035a
--- /dev/null
+++ b/sys/nixpkgs/pkgs/update_vim_plugins/poetry.lock
@@ -0,0 +1,838 @@
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
+
+[[package]]
+name = "attrs"
+version = "23.1.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
+    {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
+]
+
+[package.extras]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[docs,tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+
+[[package]]
+name = "black"
+version = "23.3.0"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
+    {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
+    {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
+    {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
+    {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
+    {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
+    {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
+    {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
+    {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
+    {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
+    {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
+    {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
+    {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
+    {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
+    {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
+    {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
+    {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
+    {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
+    {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
+    {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
+    {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
+    {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
+    {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
+    {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
+    {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "cattrs"
+version = "22.2.0"
+description = "Composable complex class support for attrs and dataclasses."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "cattrs-22.2.0-py3-none-any.whl", hash = "sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21"},
+    {file = "cattrs-22.2.0.tar.gz", hash = "sha256:f0eed5642399423cf656e7b66ce92cdc5b963ecafd041d1b24d136fdde7acf6d"},
+]
+
+[package.dependencies]
+attrs = ">=20"
+exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
+
+[[package]]
+name = "certifi"
+version = "2022.12.7"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
+    {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.1.0"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"},
+    {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"},
+    {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"},
+    {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"},
+    {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"},
+    {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
+    {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
+]
+
+[[package]]
+name = "cleo"
+version = "2.0.1"
+description = "Cleo allows you to create beautiful and testable command-line interfaces."
+category = "main"
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+    {file = "cleo-2.0.1-py3-none-any.whl", hash = "sha256:6eb133670a3ed1f3b052d53789017b6e50fca66d1287e6e6696285f4cb8ea448"},
+    {file = "cleo-2.0.1.tar.gz", hash = "sha256:eb4b2e1f3063c11085cebe489a6e9124163c226575a3c3be69b2e51af4a15ec5"},
+]
+
+[package.dependencies]
+crashtest = ">=0.4.1,<0.5.0"
+rapidfuzz = ">=2.2.0,<3.0.0"
+
+[[package]]
+name = "click"
+version = "8.1.3"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "dev"
+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.2.3"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"},
+    {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"},
+    {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"},
+    {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"},
+    {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"},
+    {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"},
+    {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"},
+    {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"},
+    {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"},
+    {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"},
+    {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"},
+    {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"},
+    {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"},
+    {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"},
+    {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"},
+    {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"},
+    {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"},
+    {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"},
+    {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"},
+    {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"},
+    {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"},
+    {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"},
+    {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"},
+    {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"},
+    {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"},
+    {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"},
+    {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"},
+    {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"},
+    {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"},
+    {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"},
+    {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"},
+    {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"},
+    {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"},
+    {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"},
+    {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"},
+    {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"},
+    {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"},
+    {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"},
+    {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"},
+    {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"},
+    {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"},
+    {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"},
+    {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"},
+    {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"},
+    {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"},
+    {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"},
+    {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"},
+    {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"},
+    {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"},
+    {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"},
+    {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"},
+]
+
+[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"
+category = "main"
+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 = "exceptiongroup"
+version = "1.1.1"
+description = "Backport of PEP 654 (exception groups)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
+    {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+    {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+    {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "isort"
+version = "5.12.0"
+description = "A Python utility / library to sort Python imports."
+category = "dev"
+optional = false
+python-versions = ">=3.8.0"
+files = [
+    {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
+    {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.3)"]
+pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
+plugins = ["setuptools"]
+requirements-deprecated-finder = ["pip-api", "pipreqs"]
+
+[[package]]
+name = "lsprotocol"
+version = "2023.0.0a1"
+description = "Python implementation of the Language Server Protocol."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "lsprotocol-2023.0.0a1-py3-none-any.whl", hash = "sha256:133c339a7cccb299a5b357f9b8ef6aebe27616e0daf4ba54e63476afe0e12e47"},
+    {file = "lsprotocol-2023.0.0a1.tar.gz", hash = "sha256:32edfd4856abba1349bf5a070567445b3d7286951afba3644b472629796f82d0"},
+]
+
+[package.dependencies]
+attrs = "*"
+cattrs = "*"
+
+[[package]]
+name = "mypy"
+version = "1.2.0"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "mypy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:701189408b460a2ff42b984e6bd45c3f41f0ac9f5f58b8873bbedc511900086d"},
+    {file = "mypy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe91be1c51c90e2afe6827601ca14353bbf3953f343c2129fa1e247d55fd95ba"},
+    {file = "mypy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d26b513225ffd3eacece727f4387bdce6469192ef029ca9dd469940158bc89e"},
+    {file = "mypy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3a2d219775a120581a0ae8ca392b31f238d452729adbcb6892fa89688cb8306a"},
+    {file = "mypy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2e93a8a553e0394b26c4ca683923b85a69f7ccdc0139e6acd1354cc884fe0128"},
+    {file = "mypy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3efde4af6f2d3ccf58ae825495dbb8d74abd6d176ee686ce2ab19bd025273f41"},
+    {file = "mypy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:695c45cea7e8abb6f088a34a6034b1d273122e5530aeebb9c09626cea6dca4cb"},
+    {file = "mypy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0e9464a0af6715852267bf29c9553e4555b61f5904a4fc538547a4d67617937"},
+    {file = "mypy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8293a216e902ac12779eb7a08f2bc39ec6c878d7c6025aa59464e0c4c16f7eb9"},
+    {file = "mypy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f46af8d162f3d470d8ffc997aaf7a269996d205f9d746124a179d3abe05ac602"},
+    {file = "mypy-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:031fc69c9a7e12bcc5660b74122ed84b3f1c505e762cc4296884096c6d8ee140"},
+    {file = "mypy-1.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:390bc685ec209ada4e9d35068ac6988c60160b2b703072d2850457b62499e336"},
+    {file = "mypy-1.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4b41412df69ec06ab141808d12e0bf2823717b1c363bd77b4c0820feaa37249e"},
+    {file = "mypy-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e4a682b3f2489d218751981639cffc4e281d548f9d517addfd5a2917ac78119"},
+    {file = "mypy-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a197ad3a774f8e74f21e428f0de7f60ad26a8d23437b69638aac2764d1e06a6a"},
+    {file = "mypy-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9a084bce1061e55cdc0493a2ad890375af359c766b8ac311ac8120d3a472950"},
+    {file = "mypy-1.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaeaa0888b7f3ccb7bcd40b50497ca30923dba14f385bde4af78fac713d6d6f6"},
+    {file = "mypy-1.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bea55fc25b96c53affab852ad94bf111a3083bc1d8b0c76a61dd101d8a388cf5"},
+    {file = "mypy-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:4c8d8c6b80aa4a1689f2a179d31d86ae1367ea4a12855cc13aa3ba24bb36b2d8"},
+    {file = "mypy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70894c5345bea98321a2fe84df35f43ee7bb0feec117a71420c60459fc3e1eed"},
+    {file = "mypy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4a99fe1768925e4a139aace8f3fb66db3576ee1c30b9c0f70f744ead7e329c9f"},
+    {file = "mypy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023fe9e618182ca6317ae89833ba422c411469156b690fde6a315ad10695a521"},
+    {file = "mypy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d19f1a239d59f10fdc31263d48b7937c585810288376671eaf75380b074f238"},
+    {file = "mypy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:2de7babe398cb7a85ac7f1fd5c42f396c215ab3eff731b4d761d68d0f6a80f48"},
+    {file = "mypy-1.2.0-py3-none-any.whl", hash = "sha256:d8e9187bfcd5ffedbe87403195e1fc340189a68463903c39e2b63307c9fa0394"},
+    {file = "mypy-1.2.0.tar.gz", hash = "sha256:f70a40410d774ae23fcb4afbbeca652905a04de7948eaf0b1789c8d1426b72d1"},
+]
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+    {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "packaging"
+version = "23.1"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
+    {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.11.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
+    {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "3.2.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
+    {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
+]
+
+[package.extras]
+docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pygls"
+version = "1.0.1"
+description = "a pythonic generic language server (pronounced like \"pie glass\")."
+category = "dev"
+optional = false
+python-versions = "<4,>=3.7"
+files = [
+    {file = "pygls-1.0.1-py3-none-any.whl", hash = "sha256:adacc96da77598c70f46acfdfd1481d3da90cd54f639f7eee52eb6e4dbd57b55"},
+    {file = "pygls-1.0.1.tar.gz", hash = "sha256:f3ee98ddbb4690eb5c755bc32ba7e129607f14cbd313575f33d0cea443b78cb2"},
+]
+
+[package.dependencies]
+lsprotocol = "*"
+typeguard = ">=2.10.0,<3"
+
+[package.extras]
+dev = ["bandit (==1.7.4)", "flake8 (==4.0.1)", "mypy (==0.961)"]
+docs = ["sphinx (==5.0.1)", "sphinx-rtd-theme (==1.0.0)"]
+test = ["mock (==4.0.3)", "pytest (==7.1.2)", "pytest-asyncio (==0.18.3)"]
+ws = ["websockets (>=10.0.0,<11.0.0)"]
+
+[[package]]
+name = "pytest"
+version = "7.3.1"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"},
+    {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"},
+]
+
+[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", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
+    {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
+]
+
+[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.10.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"},
+    {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"},
+]
+
+[package.dependencies]
+pytest = ">=5.0"
+
+[package.extras]
+dev = ["pre-commit", "pytest-asyncio", "tox"]
+
+[[package]]
+name = "rapidfuzz"
+version = "2.15.1"
+description = "rapid fuzzy string matching"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "rapidfuzz-2.15.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc0bc259ebe3b93e7ce9df50b3d00e7345335d35acbd735163b7c4b1957074d3"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d59fb3a410d253f50099d7063855c2b95df1ef20ad93ea3a6b84115590899f25"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c525a3da17b6d79d61613096c8683da86e3573e807dfaecf422eea09e82b5ba6"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4deae6a918ecc260d0c4612257be8ba321d8e913ccb43155403842758c46fbe"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2577463d10811386e704a3ab58b903eb4e2a31b24dfd9886d789b0084d614b01"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f67d5f56aa48c0da9de4ab81bffb310683cf7815f05ea38e5aa64f3ba4368339"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7927722ff43690e52b3145b5bd3089151d841d350c6f8378c3cfac91f67573a"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6534afc787e32c4104f65cdeb55f6abe4d803a2d0553221d00ef9ce12788dcde"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d0ae6ec79a1931929bb9dd57bc173eb5ba4c7197461bf69e3a34b6dd314feed2"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be7ccc45c4d1a7dfb595f260e8022a90c6cb380c2a346ee5aae93f85c96d362b"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:8ba013500a2b68c64b2aecc5fb56a2dad6c2872cf545a0308fd044827b6e5f6a"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4d9f7d10065f657f960b48699e7dddfce14ab91af4bab37a215f0722daf0d716"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e24a1b802cea04160b3fccd75d2d0905065783ebc9de157d83c14fb9e1c6ce2"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-win32.whl", hash = "sha256:dffdf03499e0a5b3442951bb82b556333b069e0661e80568752786c79c5b32de"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d150d90a7c6caae7962f29f857a4e61d42038cfd82c9df38508daf30c648ae7"},
+    {file = "rapidfuzz-2.15.1-cp310-cp310-win_arm64.whl", hash = "sha256:87c30e9184998ff6eb0fa9221f94282ce7c908fd0da96a1ef66ecadfaaa4cdb7"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6986413cb37035eb796e32f049cbc8c13d8630a4ac1e0484e3e268bb3662bd1b"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a72f26e010d4774b676f36e43c0fc8a2c26659efef4b3be3fd7714d3491e9957"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5cd54c98a387cca111b3b784fc97a4f141244bbc28a92d4bde53f164464112e"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7fac7c3da39f93e6b2ebe386ed0ffe1cefec91509b91857f6e1204509e931f"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f976e76ac72f650790b3a5402431612175b2ac0363179446285cb3c901136ca9"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abde47e1595902a490ed14d4338d21c3509156abb2042a99e6da51f928e0c117"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca8f1747007a3ce919739a60fa95c5325f7667cccf6f1c1ef18ae799af119f5e"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c35da09ab9797b020d0d4f07a66871dfc70ea6566363811090353ea971748b5a"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a3a769ca7580686a66046b77df33851b3c2d796dc1eb60c269b68f690f3e1b65"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d50622efefdb03a640a51a6123748cd151d305c1f0431af762e833d6ffef71f0"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b7461b0a7651d68bc23f0896bffceea40f62887e5ab8397bf7caa883592ef5cb"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:074ee9e17912e025c72a5780ee4c7c413ea35cd26449719cc399b852d4e42533"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7025fb105a11f503943f17718cdb8241ea3bb4d812c710c609e69bead40e2ff0"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-win32.whl", hash = "sha256:2084d36b95139413cef25e9487257a1cc892b93bd1481acd2a9656f7a1d9930c"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:5a738fcd24e34bce4b19126b92fdae15482d6d3a90bd687fd3d24ce9d28ce82d"},
+    {file = "rapidfuzz-2.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:dc3cafa68cfa54638632bdcadf9aab89a3d182b4a3f04d2cad7585ed58ea8731"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3c53d57ba7a88f7bf304d4ea5a14a0ca112db0e0178fff745d9005acf2879f7d"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6ee758eec4cf2215dc8d8eafafcea0d1f48ad4b0135767db1b0f7c5c40a17dd"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d93ba3ae59275e7a3a116dac4ffdb05e9598bf3ee0861fecc5b60fb042d539e"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c3ff75e647908ddbe9aa917fbe39a112d5631171f3fcea5809e2363e525a59d"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d89c421702474c6361245b6b199e6e9783febacdbfb6b002669e6cb3ef17a09"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f69e6199fec0f58f9a89afbbaea78d637c7ce77f656a03a1d6ea6abdc1d44f8"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:41dfea282844d0628279b4db2929da0dacb8ac317ddc5dcccc30093cf16357c1"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2dd03477feefeccda07b7659dd614f6738cfc4f9b6779dd61b262a73b0a9a178"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5efe035aa76ff37d1b5fa661de3c4b4944de9ff227a6c0b2e390a95c101814c0"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ed2cf7c69102c7a0a06926d747ed855bc836f52e8d59a5d1e3adfd980d1bd165"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a0e441d4c2025110ec3eba5d54f11f78183269a10152b3a757a739ffd1bb12bf"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-win32.whl", hash = "sha256:a4a54efe17cc9f53589c748b53f28776dfdfb9bc83619685740cb7c37985ac2f"},
+    {file = "rapidfuzz-2.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:bb8318116ecac4dfb84841d8b9b461f9bb0c3be5b616418387d104f72d2a16d1"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e9296c530e544f68858c3416ad1d982a1854f71e9d2d3dcedb5b216e6d54f067"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:49c4bcdb9238f11f8c4eba1b898937f09b92280d6f900023a8216008f299b41a"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb40a279e134bb3fef099a8b58ed5beefb201033d29bdac005bddcdb004ef71"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7381c11cb590bbd4e6f2d8779a0b34fdd2234dfa13d0211f6aee8ca166d9d05"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfdcdedfd12a0077193f2cf3626ff6722c5a184adf0d2d51f1ec984bf21c23c3"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85bece1ec59bda8b982bd719507d468d4df746dfb1988df11d916b5e9fe19e8"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b393f4a1eaa6867ffac6aef58cfb04bab2b3d7d8e40b9fe2cf40dd1d384601"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53de456ef020a77bf9d7c6c54860a48e2e902584d55d3001766140ac45c54bc7"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2492330bc38b76ed967eab7bdaea63a89b6ceb254489e2c65c3824efcbf72993"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:099e4c6befaa8957a816bdb67ce664871f10aaec9bebf2f61368cf7e0869a7a1"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:46599b2ad4045dd3f794a24a6db1e753d23304699d4984462cf1ead02a51ddf3"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:591f19d16758a3c55c9d7a0b786b40d95599a5b244d6eaef79c7a74fcf5104d8"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed17359061840eb249f8d833cb213942e8299ffc4f67251a6ed61833a9f2ea20"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-win32.whl", hash = "sha256:aa1e5aad325168e29bf8e17006479b97024aa9d2fdbe12062bd2f8f09080acf8"},
+    {file = "rapidfuzz-2.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:c2bb68832b140c551dbed691290bef4ee6719d4e8ce1b7226a3736f61a9d1a83"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3fac40972cf7b6c14dded88ae2331eb50dfbc278aa9195473ef6fc6bfe49f686"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0e456cbdc0abf39352800309dab82fd3251179fa0ff6573fa117f51f4e84be8"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:22b9d22022b9d09fd4ece15102270ab9b6a5cfea8b6f6d1965c1df7e3783f5ff"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46754fe404a9a6f5cbf7abe02d74af390038d94c9b8c923b3f362467606bfa28"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91abb8bf7610efe326394adc1d45e1baca8f360e74187f3fa0ef3df80cdd3ba6"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e40a2f60024f9d3c15401e668f732800114a023f3f8d8c40f1521a62081ff054"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a48ee83916401ac73938526d7bd804e01d2a8fe61809df7f1577b0b3b31049a3"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c71580052f9dbac443c02f60484e5a2e5f72ad4351b84b2009fbe345b1f38422"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:82b86d5b8c1b9bcbc65236d75f81023c78d06a721c3e0229889ff4ed5c858169"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fc4528b7736e5c30bc954022c2cf410889abc19504a023abadbc59cdf9f37cae"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e1e0e569108a5760d8f01d0f2148dd08cc9a39ead79fbefefca9e7c7723c7e88"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:94e1c97f0ad45b05003806f8a13efc1fc78983e52fa2ddb00629003acf4676ef"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47e81767a962e41477a85ad7ac937e34d19a7d2a80be65614f008a5ead671c56"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-win32.whl", hash = "sha256:79fc574aaf2d7c27ec1022e29c9c18f83cdaf790c71c05779528901e0caad89b"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:f3dd4bcef2d600e0aa121e19e6e62f6f06f22a89f82ef62755e205ce14727874"},
+    {file = "rapidfuzz-2.15.1-cp39-cp39-win_arm64.whl", hash = "sha256:cac095cbdf44bc286339a77214bbca6d4d228c9ebae3da5ff6a80aaeb7c35634"},
+    {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b89d1126be65c85763d56e3b47d75f1a9b7c5529857b4d572079b9a636eaa8a7"},
+    {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b7460e91168229768be882ea365ba0ac7da43e57f9416e2cfadc396a7df3c2"},
+    {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c33c03e7092642c38f8a15ca2d8fc38da366f2526ec3b46adf19d5c7aa48ba"},
+    {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040faca2e26d9dab5541b45ce72b3f6c0e36786234703fc2ac8c6f53bb576743"},
+    {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:6e2a3b23e1e9aa13474b3c710bba770d0dcc34d517d3dd6f97435a32873e3f28"},
+    {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e597b9dfd6dd180982684840975c458c50d447e46928efe3e0120e4ec6f6686"},
+    {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d14752c9dd2036c5f36ebe8db5f027275fa7d6b3ec6484158f83efb674bab84e"},
+    {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558224b6fc6124d13fa32d57876f626a7d6188ba2a97cbaea33a6ee38a867e31"},
+    {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c89cfa88dc16fd8c9bcc0c7f0b0073f7ef1e27cceb246c9f5a3f7004fa97c4d"},
+    {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:509c5b631cd64df69f0f011893983eb15b8be087a55bad72f3d616b6ae6a0f96"},
+    {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0f73a04135a03a6e40393ecd5d46a7a1049d353fc5c24b82849830d09817991f"},
+    {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c99d53138a2dfe8ada67cb2855719f934af2733d726fbf73247844ce4dd6dd5"},
+    {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f01fa757f0fb332a1f045168d29b0d005de6c39ee5ce5d6c51f2563bb53c601b"},
+    {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60368e1add6e550faae65614844c43f8a96e37bf99404643b648bf2dba92c0fb"},
+    {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785744f1270828cc632c5a3660409dee9bcaac6931a081bae57542c93e4d46c4"},
+    {file = "rapidfuzz-2.15.1.tar.gz", hash = "sha256:d62137c2ca37aea90a11003ad7dc109c8f1739bfbe5a9a217f3cdb07d7ac00f6"},
+]
+
+[package.extras]
+full = ["numpy"]
+
+[[package]]
+name = "requests"
+version = "2.28.2"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+files = [
+    {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
+    {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "ruff"
+version = "0.0.262"
+description = "An extremely fast Python linter, written in Rust."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "ruff-0.0.262-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:c26c1abd420d041592d05d63aee8c6a18feb24aed4deb6e91129e9f2c7b4914a"},
+    {file = "ruff-0.0.262-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b379e9765afa679316e52288a942df085e590862f8945088936a7bce3116d8f3"},
+    {file = "ruff-0.0.262-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7e0ca6821aafbd2b059df3119fcd5881250721ca8e825789fd2c471f7c59be"},
+    {file = "ruff-0.0.262-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cca35e2aeddff72bb4379a1dabc134e0c0d25ebc754a2cb733a1f8d4dbbb5e0"},
+    {file = "ruff-0.0.262-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15bf5533ce169aebbafa00017987f673e879f60a625d932b464b8cdaf32a4fce"},
+    {file = "ruff-0.0.262-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3909e249d984c4517194005a1c30eaa0c3a6d906c789d9fc0c9c7e007fb3e759"},
+    {file = "ruff-0.0.262-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e2813013a19b3e147e840bdb2e42db5825b53b47364e58e7b467c5fa47ffda2"},
+    {file = "ruff-0.0.262-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d25a94996b2037e566c2a801c8b324c0a826194d5d4d90ad7c1ccb8cf06521fa"},
+    {file = "ruff-0.0.262-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ca04348372efc59f6ee808d903d35e0d352cf2c78e487757cd48b65104b83e"},
+    {file = "ruff-0.0.262-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:24f989363e9bb5d0283490298102a5218682e49ebf300e445d69e24bee03ac83"},
+    {file = "ruff-0.0.262-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3c24e678e43ca4b67e29cc9a7a54eea05f31a5898cbf17bfec47b68f08d32a60"},
+    {file = "ruff-0.0.262-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0baff3c9a22227358ea109c165efe62dbdd0f2b9fd5256567dda8682b444fe23"},
+    {file = "ruff-0.0.262-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:083bac6e238d8b7d5ac3618666ea63b7ac661cf94c5da160070a58e190082831"},
+    {file = "ruff-0.0.262-py3-none-win32.whl", hash = "sha256:15bbfa2d15c137717627e0d56b0e535ae297b734551e34e03fcc25d7642cf43a"},
+    {file = "ruff-0.0.262-py3-none-win_amd64.whl", hash = "sha256:973ac29193f718349cf5746b7d86dfeaf7d40e9651ed97790a9b9327305888b9"},
+    {file = "ruff-0.0.262-py3-none-win_arm64.whl", hash = "sha256:f102904ebe395acd2a181d295b98120acd7a63f732b691672977fc688674f4af"},
+    {file = "ruff-0.0.262.tar.gz", hash = "sha256:faea54231c265f5349975ba6f3d855b71881a01f391b2000c47740390c6d5f68"},
+]
+
+[[package]]
+name = "ruff-lsp"
+version = "0.0.24"
+description = "A Language Server Protocol implementation for Ruff."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "ruff_lsp-0.0.24-py3-none-any.whl", hash = "sha256:71207b0a64f40577db3e9aef0b1331fcf4122f9b98bba66b905d41706d639e65"},
+    {file = "ruff_lsp-0.0.24.tar.gz", hash = "sha256:d617bf19893c3bd2ea3d71f79aeede196b91ca08831b53a727e24d4f63f29f3a"},
+]
+
+[package.dependencies]
+lsprotocol = ">=2023.0.0a1"
+pygls = ">=1.0.1"
+ruff = ">=0.0.253"
+typing-extensions = "*"
+
+[package.extras]
+dev = ["black (==22.12.0)", "mypy (==0.991)", "python-lsp-jsonrpc (==1.0.0)"]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+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 = "typeguard"
+version = "2.13.3"
+description = "Run-time type checker for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.5.3"
+files = [
+    {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"},
+    {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"},
+]
+
+[package.extras]
+doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["mypy", "pytest", "typing-extensions"]
+
+[[package]]
+name = "types-requests"
+version = "2.28.11.17"
+description = "Typing stubs for requests"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "types-requests-2.28.11.17.tar.gz", hash = "sha256:0d580652ce903f643f8c3b494dd01d29367ea57cea0c7ad7f65cf3169092edb0"},
+    {file = "types_requests-2.28.11.17-py3-none-any.whl", hash = "sha256:cc1aba862575019306b2ed134eb1ea994cab1c887a22e18d3383e6dd42e9789b"},
+]
+
+[package.dependencies]
+types-urllib3 = "<1.27"
+
+[[package]]
+name = "types-urllib3"
+version = "1.26.25.10"
+description = "Typing stubs for urllib3"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "types-urllib3-1.26.25.10.tar.gz", hash = "sha256:c44881cde9fc8256d05ad6b21f50c4681eb20092552351570ab0a8a0653286d6"},
+    {file = "types_urllib3-1.26.25.10-py3-none-any.whl", hash = "sha256:12c744609d588340a07e45d333bf870069fc8793bcf96bae7a96d4712a42591d"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.5.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
+    {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+]
+
+[[package]]
+name = "urllib3"
+version = "1.26.15"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+files = [
+    {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"},
+    {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.10"
+content-hash = "519ec995a8bf87cd80caa4e18e100450209fbbf6970296ea08ad71f27586644a"
diff --git a/sys/nixpkgs/pkgs/update_vim_plugins/pyproject.toml b/sys/nixpkgs/pkgs/update_vim_plugins/pyproject.toml
new file mode 100644
index 00000000..d08dc043
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/__init__.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/__init__.py
diff --git a/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/__main__.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/__main__.py
new file mode 100644
index 00000000..a8d9e06f
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/cleanup.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/cleanup.py
new file mode 100644
index 00000000..fd313ed0
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/helpers.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/helpers.py
new file mode 100644
index 00000000..8a28b0e8
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/nix.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/nix.py
new file mode 100644
index 00000000..66a8df4c
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/plugin.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/plugin.py
new file mode 100644
index 00000000..8334ad53
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/spec.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/spec.py
new file mode 100644
index 00000000..04f25ccc
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/__init__.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/__init__.py
diff --git a/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/fixtures.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/fixtures.py
new file mode 100644
index 00000000..a90d67c6
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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, License, 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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_nix.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_nix.py
new file mode 100644
index 00000000..5d6e51b1
--- /dev/null
+++ b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_nix.py
@@ -0,0 +1,34 @@
+from update_vim_plugins.nix import GitSource, License, UrlSource
+
+from .fixtures import git_source, rev, sha256, url, url_source
+
+
+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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_plugin.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_plugin.py
new file mode 100644
index 00000000..27d1a8db
--- /dev/null
+++ b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_plugin.py
@@ -0,0 +1,146 @@
+import json
+from typing import Callable
+
+import pytest
+from pytest_mock import MockFixture
+
+from update_vim_plugins.nix import License, Source, UrlSource
+from update_vim_plugins.plugin import GitHubPlugin, VimPlugin
+from update_vim_plugins.spec import PluginSpec
+
+from .fixtures import sha256, url, url_source
+
+
+@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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_spec.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/tests/test_spec.py
new file mode 100644
index 00000000..2b9a1d24
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/update.py b/sys/nixpkgs/pkgs/update_vim_plugins/update_vim_plugins/update.py
new file mode 100644
index 00000000..7eb3eeb4
--- /dev/null
+++ b/sys/nixpkgs/pkgs/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