InstallationΒΆ

Mail Deduplicate is distributed on PyPI.

Install the latest stable release with uv:

$ uv tool install mail-deduplicate

Try itΒΆ

You can try Mail Deduplicate right now in your terminal, without installing any dependency or virtual env thanks to uvx:

$ uvx --from mail-deduplicate -- mdedup --help
$ mdedup --help
Usage: mdedup [OPTIONS] MAIL_SOURCE_1 MAIL_SOURCE_2 ...

  Deduplicate mails from multiple sources.

  Process:
  - Step #1: load mails from their sources.
  - Step #2: compute the canonical hash of each mail based on their headers (and
             optionally their body), and regroup mails sharing the same hash.
  - Step #3: apply a selection strategy on each subset of duplicate mails.
  - Step #4: perform an action on all selected mails.
  - Step #5: report statistics.

Positional arguments:
  MAIL_SOURCE_1 MAIL_SOURCE_2 ...
     Mail sources to deduplicate. Can be a single mail box or a list of mails.

Mail sources (step #1):
  -i, --input-format [maildir|mbox|mh|babyl|mmdf|eml]
     Force all provided mail sources to be parsed in the specified format. If
     not set, auto-detect the format of sources independently. Auto-detection
     only supports maildir and mbox format. Use this option to open up other box
     format, or bypass unreliable detection.

  -u, --force-unlock
     Remove the lock on mail source opening if one is found.

Hashing (step #2):
  -h, --hash-header Header-ID
     Headers to use to compute each mail's hash. Must be repeated multiple times
     to set an ordered list of headers. Header IDs are case-insensitive.
     Repeating entries are ignored.  [default: Date, From, To, Subject, MIME-
     Version, Content-Type, Content-Disposition, User-Agent, X-Priority,
     Message-ID]

  -b, --hash-body [skip|raw|normalized]
     Method used to hash the body of mails. Defaults to skip, which doesn't hash
     the body at all: it is the fastest method and header-based hash should be
     sufficient to determine duplicate set. raw use the body as it is (slow).
     normalized pre-process the body before hashing, by removing all line breaks
     and spaces (slowest).  [default: skip]

  -H, --hash-only
     Compute and display the internal hashes used to identify duplicates. Do not
     performs any selection or action.

  --cache / --no-cache
     Reuse the hashes computed by previous runs, so unchanged mails are not read
     nor parsed again. An entry is only trusted while the size and modification
     time of the file backing its mail are unchanged, and the whole cache is
     discarded as soon as any option feeding the hashes changes. Mails of a
     file-based box (mbox, babyl, mmdf) all share the box's file, so editing it
     invalidates every one of them at once. Off by default.  [default: no-cache]

  --cache-path CACHE_DB_PATH
     Location of the hash cache database. Implies --cache. Defaults to
     /home/runner/.cache/mdedup/hashes.db

Deduplication (step #3):
  Process each set of mails sharing the same hash and apply the selection
  --strategy. Fine-grained checks on size and content are performed beforehand.
  Mails differing above safety levels are set aside so the rest can still be
  deduplicated, and the set is skipped if fewer than 2 remain. Limits can be set
  via the --size-threshold and --content-threshold options.
  -s, --strategy [select-older|select-oldest|select-newer|select-newest|discard-newest|discard-newer|discard-oldest|discard-older|select-smaller|select-smallest|select-bigger|select-biggest|discard-biggest|discard-bigger|discard-smallest|discard-smaller|select-matching-path|select-non-matching-path|discard-non-matching-path|discard-matching-path|select-one|select-all-but-one|discard-all-but-one|discard-one]
     Selection strategy to apply within a subset of duplicates. Can be repeated
     multiple times to set an ordered list of fallback strategies: each
     duplicate set is handed over to the next strategy each time a strategy
     fails to discriminate its mails, by selecting all of them, none of them, or
     by missing the timestamps to compare them. Repeating entries are ignored,
     including aliases of strategies already listed. If not set, duplicates will
     be grouped and counted but all be skipped, selection will be empty, and no
     action will be performed. Description of each strategy is available further
     down that help screen.

  -t, --time-source [date-header|ctime]
     Source of a mail's time reference used in time-sensitive strategies.
     [default: date-header]

  -r, --regexp REGEXP
     Regular expression on a mail's file path. Applies to individual mail
     location for folder-based boxes (maildir, mh, eml). But for file-based
     boxes (mbox, babyl, mmdf), applies to the whole box's path, as all mails
     are packed into one single file. Required in discard-matching-path,
     discard-non-matching-path, select-matching-path and select-non-matching-
     path strategies.

  -S, --size-threshold BYTES
     Maximum difference allowed in size between mails sharing the same hash.
     Mails in an offending pair are set aside until the rest all pass. The
     subset is skipped if fewer than 2 remain. Set to 0 to enforce strictness
     and apply selection strategy on the subset only if all mails are exactly
     the same. Set to -1 to allow any difference and apply the strategy whatever
     the differences.  [default: 512; x>=-1]

  -C, --content-threshold BYTES
     Maximum difference allowed in content between mails sharing the same hash.
     Mails in an offending pair are set aside until the rest all pass. The
     subset is skipped if fewer than 2 remain. Set to 0 to enforce strictness
     and apply selection strategy on the subset only if all mails are exactly
     the same. Set to -1 to allow any difference and apply the strategy whatever
     the differences.  [default: 768; x>=-1]

  -d, --show-diff
     Show the unified diff of duplicates not within thresholds.

Action (step #4):
  -a, --action [copy-selected|copy-discarded|move-selected|move-discarded|delete-selected|delete-discarded|hardlink-discarded]
     Action performed on the selected mails. Defaults to copy-selected as it is
     the safest: it only reads the mail sources and create a brand new mail box
     with the selection results.  [default: copy-selected]

  -E, --export MAIL_BOX_PATH
     Location of the destination mail box to where to copy or move deduplicated
     mails. Required in copy-selected, copy-discarded, move-selected and move-
     discarded actions.

  -e, --export-format [maildir|mbox|mh|babyl|mmdf|eml]
     Format of the mail box to which deduplication mails will be exported to.
     Only affects copy-selected, copy-discarded, move-selected and move-
     discarded actions.  [default: mbox]

  --export-append
     If destination mail box already exists, add mails into it instead of
     interrupting (default behavior). Affect copy-selected, copy-discarded,
     move-selected and move-discarded actions.

  --hardlink-differing
     Hardlink discarded mails whose content differs byte for byte from the copy
     they are linked to, instead of leaving them alone. Their own content is
     then swapped for that copy's, so whatever was unique to them, like the
     headers a mail collects on its way to one account, is lost. Only affects
     the hardlink-discarded action.

  -n, --dry-run
     Do not perform any action but act as if it was, and report which action
     would have been performed otherwise.

Other options:
  --time / --no-time
     Measure and print elapsed execution time.  [default: no-time]

  --config CONFIG_PATH
     Location of the configuration file. Supports local path with glob patterns
     or remote URL.  [default:
     ~/.config/mdedup/{*.toml,*.yaml,*.yml,*.json,*.ini,pyproject.toml}]

  --no-config
     Ignore all configuration files and only use command line parameters and
     environment variables.

  --validate-config FILE
     Validate the configuration file and exit.

  --export-config FORMAT
     Export the configuration in the selected format to <stdout>, then exit.

  --accessible
     Accessibility mode: disable colors and render tables in a plain, screen-
     reader-friendly format.

  --color [auto|always|never]
     Colorize the output. A bare --color is the same as --color=always.
     [default: auto]

  --no-color
     Disable colorization (alias of --color=never).

  --progress / --no-progress
     Show progress indicators during long operations. Disabled for non-
     interactive output (pipes, dumb terminals, CI) and by --accessible.
     [default: progress]

  --theme [dark|dracula|light|manpage|monokai|nord|solarized_dark]
     Color theme used for help screens.  [default: dark]

  --params
     Show all CLI parameters, their provenance, defaults and value, then exit.

  --table-format [aligned|asciidoc|colon-grid|csv|csv-excel|csv-excel-tab|csv-unix|double-grid|double-outline|fancy-grid|fancy-outline|github|grid|heavy-grid|heavy-outline|hjson|html|jira|json|json5|jsonc|latex|latex-booktabs|latex-longtable|latex-raw|mediawiki|mixed-grid|mixed-outline|moinmoin|orgtbl|outline|pipe|plain|presto|pretty|psql|rounded-grid|rounded-outline|rst|simple|simple-grid|simple-outline|textile|toml|tsv|unsafehtml|vertical|xml|yaml|youtrack]
     Rendering style of tables.  [default: rounded-outline]

  --verbosity LEVEL
     Either CRITICAL, ERROR, WARNING, INFO, DEBUG.  [default: INFO]

  -v, --verbose
     Increase the default WARNING verbosity by one level for each additional
     repetition of the option.  [default: 0]

  -q, --quiet
     Decrease the default WARNING verbosity by one level for each additional
     repetition of the option.  [default: 0]

  --tree
     Show the tree of nested subcommands and exit.

  --man
     Show the command's man page (roff) and exit.

  --version
     Show the version and exit.

  --jobs [auto|max|INTEGER]
     Number of parallel jobs used to hash mails (step #2) and settle duplicate
     sets (step #3). Accepts an integer, 'auto' (one fewer than the host's
     logical CPUs) or 'max'. Defaults to 1 (sequential). Only folder-based boxes
     fan out; higher values speed up --hash-body raw/normalized and boxes
     holding many duplicate sets.  [default: 1]

  --help
     Show this message and exit.

Constraints:
  {--regexp}
     all required if -s/--strategy is one of discard-matching-path, discard-non-
     matching-path, select-matching-path, select-non-matching-path

  {--regexp}
     all forbidden if -s/--strategy is none of discard-matching-path, discard-
     non-matching-path, select-matching-path, select-non-matching-path

  {--export}
     all required if -a/--action is one of copy-discarded, copy-selected, move-
     discarded, move-selected

  {--export}
     all forbidden if -a/--action is none of copy-discarded, copy-selected,
     move-discarded, move-selected

Available strategies:
  [select-all-but-one|discard-one]
     Randomly discard one duplicate, and select all others.

  [select-bigger|discard-smallest]
     Select all bigger duplicates. Discards the smallests, i.e. the subset
     sharing the smallest size.

  [select-biggest|discard-smaller]
     Select all the biggest duplicates. Discards the smallers, i.e. all mail of
     the duplicate set but those sharing the biggest size.

  [select-matching-path|discard-non-matching-path]
     Select all duplicates whose file path match the regular expression provided
     via the --regexp parameter.

  [select-newer|discard-oldest]
     Select all newer duplicates. Discards the oldest, i.e. the subset sharing
     the most ancient timestamp.

  [select-newest|discard-older]
     Select all the newest duplicates. Discards the olders, i.e. all mail of the
     duplicate set but those sharing the newest timestamp.

  [select-non-matching-path|discard-matching-path]
     Select all duplicates whose file path doesn't match the regular expression
     provided via the --regexp parameter.

  [select-older|discard-newest]
     Select all older duplicates. Discards the newests, i.e. the subset sharing
     the most recent timestamp.

  [select-oldest|discard-newer]
     Select all the oldest duplicates. Discards the newers, i.e. all mail of the
     duplicate set but those sharing the oldest timestamp.

  [select-one|discard-all-but-one]
     Randomly select one duplicate, and discards all others.

  [select-smaller|discard-biggest]
     Select all smaller duplicates. Discards the biggests, i.e. the subset
     sharing the biggest size.

  [select-smallest|discard-bigger]
     Select all the smallest duplicates. Discards the biggers. i.e. all mail of
     the duplicate set but those sharing the smallest size.
$ uvx --from mail-deduplicate@9.3.0 -- mdedup
$ uvx --from git+https://github.com/kdeldycke/mail-deduplicate -- mdedup

This will download mail-deduplicate (the package), and run mdedup, the CLI included in the package.

Installation methodsΒΆ

Easiest way is to install uv, then install mail-deduplicate system-wide, with the uv tool command:

$ uv tool install mail-deduplicate

Then you can run mdedup directly:

$ mdedup --version

You can install the latest stable release and its dependencies with a simple pip call:

$ python -m pip install mail-deduplicate

Other variations includes:

$ pip install mail-deduplicate
$ pip3 install mail-deduplicate

If you have difficulties to use pip, see pip’s own installation instructions.

pipx is a great way to install Python applications globally:

$ pipx install mail-deduplicate

An mdedup package is available on AUR and can be installed with any AUR helper:

$ pacaur -S mail-deduplicate
$ pacman -S mail-deduplicate
$ paru -S mail-deduplicate
$ yay -S mail-deduplicate

Mail Deduplicate is available as an Homebrew formula, so you just need to:

$ brew install mail-deduplicate

Python compatibilityΒΆ

The table below shows which Python versions each mail-deduplicate release range supports, derived from the declarations in each git tag’s pyproject.toml. It is refreshed by click-extra’s {matrix} directive machinery through the update-docs job.

mail-deduplicate

Released

3.14

3.13

3.12

3.11

3.10

3.9

3.8

3.7

3.6

3.5

3.4

3.3

2.7

8.0.x β†’ 9.x

2025-11-21

βœ…

βœ…

βœ…

βœ…

βœ…

❌

❌

❌

❌

❌

❌

❌

❌

7.6.x

2024-11-24

❌

βœ…

βœ…

βœ…

βœ…

❌

❌

❌

❌

❌

❌

❌

❌

7.5.0

2024-07-03

❌

❌

βœ…

βœ…

βœ…

βœ…

❌

❌

❌

❌

❌

❌

❌

7.3.x β†’ 7.4.x

2023-11-14

❌

❌

βœ…

βœ…

βœ…

βœ…

βœ…

❌

❌

❌

❌

❌

❌

7.0.x β†’ 7.2.x

2022-11-26

❌

❌

βœ…

βœ…

βœ…

βœ…

βœ…

βœ…

❌

❌

❌

❌

❌

6.1.x β†’ 6.2.x

2021-01-26

❌

❌

❌

βœ…

βœ…

βœ…

βœ…

βœ…

βœ…

❌

❌

❌

❌

4.0.x β†’ 6.0.x

2020-10-02

❌

❌

❌

❌

❌

βœ…

βœ…

βœ…

❌

❌

❌

❌

❌

2.2.x β†’ 3.0.x

2020-09-03

❌

❌

❌

❌

❌

❌

βœ…

βœ…

βœ…

βœ…

βœ…

❌

βœ…

2.0.x β†’ 2.1.x

2016-11-13

❌

❌

❌

❌

❌

❌

βœ…

βœ…

βœ…

βœ…

βœ…

βœ…

βœ…

BinariesΒΆ

Binaries are compiled at each release, so you can skip the installation process above and download the standalone executables directly.

This is the preferred way of testing mdedup without polluting your machine. They also offer the possibility of running the CLI on older systems not supporting the minimal Python version required by mdedup.

All links above points to the latest released version of mdedup.

Binaries of all past releases, with their VirusTotal analyses, are cataloged on the binaries page.

See also

Older releases If you need to test previous versions for regression, compatibility or general troubleshooting, you’ll find the old binaries attached as assets to past releases on GitHub.

Caution

Development builds Each commit to the development branch triggers the compilation of binaries. This way you can easily test the bleeding edge version of mdedup and report any issue.

Look at the list of latest binary builds. Then select the latest Build & release/release.yaml workflow run and download the binary artifact corresponding to your platform and architecture.

Note

ABI targets

$ file ./mdedup*
./mdedup-linux-arm64.bin:   ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=520bfc6f2bb21f48ad568e46752888236552b26a, for GNU/Linux 3.7.0, stripped
./mdedup-linux-x64.bin:     ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=56ba24bccfa917e6ce9009223e4e83924f616d46, for GNU/Linux 3.2.0, stripped
./mdedup-macos-arm64.bin:   Mach-O 64-bit executable arm64
./mdedup-macos-x64.bin:     Mach-O 64-bit executable x86_64
./mdedup-windows-arm64.exe: PE32+ executable (console) Aarch64, for MS Windows
./mdedup-windows-x64.exe:   PE32+ executable (console) x86-64, for MS Windows

Python module usageΒΆ

Mail Deduplicate should now be available system-wide:

$ mdedup --version
mdedup, version 9.1.0

If not, you can directly execute the module from Python:

$ python -m mail_deduplicate --version

Shell completionΒΆ

Completion for popular shell rely on Click feature.

Add this to ~/.bashrc:

eval "$(_MDEDUP_COMPLETE=bash_source mdedup)"

Add this to ~/.zshrc:

eval "$(_MDEDUP_COMPLETE=zsh_source mdedup)"

Add this to ~/.config/fish/completions/mdedup.fish:

eval (env _MDEDUP_COMPLETE=fish_source mdedup)

Alternatively, export the generated completion code as a static script to be executed:

$ _MDEDUP_COMPLETE=bash_source mdedup > ~/.mdedup-complete.bash

Then source it from ~/.bashrc:

. ~/.mdedup-complete.bash
$ _MDEDUP_COMPLETE=zsh_source mdedup > ~/.mdedup-complete.zsh

Then source it from ~/.zshrc:

. ~/.mdedup-complete.zsh
_MDEDUP_COMPLETE=fish_source mdedup > ~/.config/fish/completions/mdedup.fish

Python dependenciesΒΆ

FYI, here is a graph of Python package dependencies:

        flowchart LR
    mail_deduplicate[["`mail-deduplicate`"]]

    subgraph primary-deps [Primary dependencies]
        click_extra{{"`click-extra`"}}
        extra_platforms{{"`extra-platforms`"}}
        boltons{{"`boltons`"}}
        backports_strenum{{"`backports-strenum`"}}
        platformdirs{{"`platformdirs`"}}
    end

    pygments(["`pygments`"])
    requests(["`requests`"])
    jinja2(["`jinja2`"])
    beautifulsoup4(["`beautifulsoup4`"])
    click_0(["`click`"])
    cloup(["`cloup`"])
    colorama(["`colorama`"])
    docutils(["`docutils`"])
    markdown_it_py(["`markdown-it-py`"])
    tomli(["`tomli`"])
    typing_extensions(["`typing-extensions`"])
    accessible_pygments(["`accessible-pygments`"])
    exceptiongroup(["`exceptiongroup`"])
    mdit_py_plugins(["`mdit-py-plugins`"])
    packaging(["`packaging`"])
    pluggy(["`pluggy`"])
    pyyaml(["`pyyaml`"])
    sphinx_basic_ng(["`sphinx-basic-ng`"])
    tabulate(["`tabulate`"])
    tzdata(["`tzdata`"])
    tzlocal(["`tzlocal`"])
    wcmatch(["`wcmatch`"])
    wcwidth(["`wcwidth`"])
    alabaster(["`alabaster`"])
    babel(["`babel`"])
    bracex(["`bracex`"])
    certifi(["`certifi`"])
    charset_normalizer(["`charset-normalizer`"])
    deepmerge(["`deepmerge`"])
    execnet(["`execnet`"])
    idna(["`idna`"])
    imagesize(["`imagesize`"])
    iniconfig(["`iniconfig`"])
    markupsafe(["`markupsafe`"])
    mdurl(["`mdurl`"])
    psutil(["`psutil`"])
    roman_numerals(["`roman-numerals`"])
    snowballstemmer(["`snowballstemmer`"])
    soupsieve(["`soupsieve`"])
    sphinxcontrib_applehelp(["`sphinxcontrib-applehelp`"])
    sphinxcontrib_devhelp(["`sphinxcontrib-devhelp`"])
    sphinxcontrib_htmlhelp(["`sphinxcontrib-htmlhelp`"])
    sphinxcontrib_jsmath(["`sphinxcontrib-jsmath`"])
    sphinxcontrib_qthelp(["`sphinxcontrib-qthelp`"])
    sphinxcontrib_serializinghtml(["`sphinxcontrib-serializinghtml`"])
    tomlkit(["`tomlkit`"])
    urllib3(["`urllib3`"])

    subgraph grp_docs [--group docs]
        sphinx{{"`sphinx >=8`"}}
        myst_parser{{"`myst-parser >=5.1`"}}
        furo{{"`furo >=2025.9.25`"}}
        sphinxcontrib_mermaid{{"`sphinxcontrib-mermaid >=1.2.3`"}}
        sphinx_autodoc_typehints{{"`sphinx-autodoc-typehints >=2.4`"}}
        sphinx_copybutton{{"`sphinx-copybutton >=0.5.2`"}}
        sphinx_design{{"`sphinx-design >=0.6`"}}
        sphinxext_opengraph{{"`sphinxext-opengraph >=0.13`"}}
    end

    subgraph grp_test [--group test]
        pytest{{"`pytest >=9`"}}
        pytest_cov{{"`pytest-cov >=4`"}}
        pytest_xdist{{"`pytest-xdist >=3.7`"}}
        coverage{{"`coverage >=7.11`"}}
        whenever{{"`whenever >=0.10`"}}
        pytest_github_actions_annotate_failures{{"`pytest-github-actions-annotate-failures >=0.2`"}}
        pytest_randomly{{"`pytest-randomly >=3`"}}
    end

    subgraph grp_typing [--group typing]
        types_boltons{{"`types-boltons >=25.0.0.20250822`"}}
    end

    mail_deduplicate ==>|" >=8.7 "| click_extra
    mail_deduplicate ==>|" >=2 "| extra_platforms
    mail_deduplicate ==>|" >=25 "| boltons
    mail_deduplicate ==>|" >=1 "| backports_strenum
    mail_deduplicate ==>|" >=4.5 "| platformdirs
    sphinx --> pygments
    sphinx --> requests
    sphinx --> jinja2
    sphinx --> colorama
    sphinx --> docutils
    sphinx --> packaging
    sphinx --> alabaster
    sphinx --> babel
    sphinx --> imagesize
    sphinx --> roman_numerals
    sphinx --> snowballstemmer
    sphinx --> sphinxcontrib_applehelp
    sphinx --> sphinxcontrib_devhelp
    sphinx --> sphinxcontrib_htmlhelp
    sphinx --> sphinxcontrib_jsmath
    sphinx --> sphinxcontrib_qthelp
    sphinx --> sphinxcontrib_serializinghtml
    click_extra --> sphinx
    click_extra --> pytest
    click_extra --> pygments
    click_extra --> click_0
    click_extra --> cloup
    click_extra --> docutils
    click_extra --> extra_platforms
    click_extra --> tomli
    click_extra --> boltons
    click_extra --> tabulate
    click_extra --> wcmatch
    click_extra --> wcwidth
    click_extra --> deepmerge
    click_extra --> tomlkit
    pytest --> pygments
    pytest --> colorama
    pytest --> tomli
    pytest --> exceptiongroup
    pytest --> packaging
    pytest --> pluggy
    pytest --> iniconfig
    myst_parser --> sphinx
    myst_parser --> jinja2
    myst_parser --> docutils
    myst_parser --> markdown_it_py
    myst_parser --> mdit_py_plugins
    myst_parser --> pyyaml
    furo --> sphinx
    furo --> pygments
    furo --> beautifulsoup4
    furo --> accessible_pygments
    furo --> sphinx_basic_ng
    pytest_cov --> pytest
    pytest_cov --> coverage
    pytest_cov --> pluggy
    pytest_xdist --> pytest
    pytest_xdist --> execnet
    pytest_xdist --> psutil
    sphinxcontrib_mermaid --> sphinx
    sphinxcontrib_mermaid --> jinja2
    sphinxcontrib_mermaid --> pyyaml
    coverage --> tomli
    extra_platforms --> pytest
    whenever --> tzdata
    whenever --> tzlocal
    pytest_github_actions_annotate_failures --> pytest
    pytest_randomly --> pytest
    sphinx_autodoc_typehints --> sphinx
    sphinx_copybutton --> sphinx
    sphinx_design --> sphinx
    sphinxext_opengraph --> sphinx
    requests --> certifi
    requests --> charset_normalizer
    requests --> idna
    requests --> urllib3
    jinja2 --> markupsafe
    beautifulsoup4 --> typing_extensions
    beautifulsoup4 --> soupsieve
    click_0 --> colorama
    cloup --> click_0
    cloup --> typing_extensions
    markdown_it_py --> mdurl
    accessible_pygments --> pygments
    exceptiongroup --> typing_extensions
    mdit_py_plugins --> markdown_it_py
    sphinx_basic_ng --> sphinx
    tabulate --> wcwidth
    tzlocal --> tzdata
    wcmatch --> bracex
    mail_deduplicate -.-> grp_docs
    mail_deduplicate -.-> grp_test
    mail_deduplicate -.-> grp_typing

    click accessible_pygments "https://pypi.org/project/accessible-pygments/" _blank
    click alabaster "https://pypi.org/project/alabaster/" _blank
    click babel "https://pypi.org/project/babel/" _blank
    click backports_strenum "https://pypi.org/project/backports-strenum/" _blank
    click beautifulsoup4 "https://pypi.org/project/beautifulsoup4/" _blank
    click boltons "https://pypi.org/project/boltons/" _blank
    click bracex "https://pypi.org/project/bracex/" _blank
    click certifi "https://pypi.org/project/certifi/" _blank
    click charset_normalizer "https://pypi.org/project/charset-normalizer/" _blank
    click click_0 "https://pypi.org/project/click/" _blank
    click click_extra "https://pypi.org/project/click-extra/" _blank
    click cloup "https://pypi.org/project/cloup/" _blank
    click colorama "https://pypi.org/project/colorama/" _blank
    click coverage "https://pypi.org/project/coverage/" _blank
    click deepmerge "https://pypi.org/project/deepmerge/" _blank
    click docutils "https://pypi.org/project/docutils/" _blank
    click exceptiongroup "https://pypi.org/project/exceptiongroup/" _blank
    click execnet "https://pypi.org/project/execnet/" _blank
    click extra_platforms "https://pypi.org/project/extra-platforms/" _blank
    click furo "https://pypi.org/project/furo/" _blank
    click idna "https://pypi.org/project/idna/" _blank
    click imagesize "https://pypi.org/project/imagesize/" _blank
    click iniconfig "https://pypi.org/project/iniconfig/" _blank
    click jinja2 "https://pypi.org/project/jinja2/" _blank
    click mail_deduplicate "https://pypi.org/project/mail-deduplicate/" _blank
    click markdown_it_py "https://pypi.org/project/markdown-it-py/" _blank
    click markupsafe "https://pypi.org/project/markupsafe/" _blank
    click mdit_py_plugins "https://pypi.org/project/mdit-py-plugins/" _blank
    click mdurl "https://pypi.org/project/mdurl/" _blank
    click myst_parser "https://pypi.org/project/myst-parser/" _blank
    click packaging "https://pypi.org/project/packaging/" _blank
    click platformdirs "https://pypi.org/project/platformdirs/" _blank
    click pluggy "https://pypi.org/project/pluggy/" _blank
    click psutil "https://pypi.org/project/psutil/" _blank
    click pygments "https://pypi.org/project/pygments/" _blank
    click pytest "https://pypi.org/project/pytest/" _blank
    click pytest_cov "https://pypi.org/project/pytest-cov/" _blank
    click pytest_github_actions_annotate_failures "https://pypi.org/project/pytest-github-actions-annotate-failures/" _blank
    click pytest_randomly "https://pypi.org/project/pytest-randomly/" _blank
    click pytest_xdist "https://pypi.org/project/pytest-xdist/" _blank
    click pyyaml "https://pypi.org/project/pyyaml/" _blank
    click requests "https://pypi.org/project/requests/" _blank
    click roman_numerals "https://pypi.org/project/roman-numerals/" _blank
    click snowballstemmer "https://pypi.org/project/snowballstemmer/" _blank
    click soupsieve "https://pypi.org/project/soupsieve/" _blank
    click sphinx "https://pypi.org/project/sphinx/" _blank
    click sphinx_autodoc_typehints "https://pypi.org/project/sphinx-autodoc-typehints/" _blank
    click sphinx_basic_ng "https://pypi.org/project/sphinx-basic-ng/" _blank
    click sphinx_copybutton "https://pypi.org/project/sphinx-copybutton/" _blank
    click sphinx_design "https://pypi.org/project/sphinx-design/" _blank
    click sphinxcontrib_applehelp "https://pypi.org/project/sphinxcontrib-applehelp/" _blank
    click sphinxcontrib_devhelp "https://pypi.org/project/sphinxcontrib-devhelp/" _blank
    click sphinxcontrib_htmlhelp "https://pypi.org/project/sphinxcontrib-htmlhelp/" _blank
    click sphinxcontrib_jsmath "https://pypi.org/project/sphinxcontrib-jsmath/" _blank
    click sphinxcontrib_mermaid "https://pypi.org/project/sphinxcontrib-mermaid/" _blank
    click sphinxcontrib_qthelp "https://pypi.org/project/sphinxcontrib-qthelp/" _blank
    click sphinxcontrib_serializinghtml "https://pypi.org/project/sphinxcontrib-serializinghtml/" _blank
    click sphinxext_opengraph "https://pypi.org/project/sphinxext-opengraph/" _blank
    click tabulate "https://pypi.org/project/tabulate/" _blank
    click tomli "https://pypi.org/project/tomli/" _blank
    click tomlkit "https://pypi.org/project/tomlkit/" _blank
    click types_boltons "https://pypi.org/project/types-boltons/" _blank
    click typing_extensions "https://pypi.org/project/typing-extensions/" _blank
    click tzdata "https://pypi.org/project/tzdata/" _blank
    click tzlocal "https://pypi.org/project/tzlocal/" _blank
    click urllib3 "https://pypi.org/project/urllib3/" _blank
    click wcmatch "https://pypi.org/project/wcmatch/" _blank
    click wcwidth "https://pypi.org/project/wcwidth/" _blank
    click whenever "https://pypi.org/project/whenever/" _blank

    style mail_deduplicate stroke-width:3px
    style backports_strenum stroke-width:3px
    style boltons stroke-width:3px
    style click_extra stroke-width:3px
    style coverage stroke-width:3px
    style extra_platforms stroke-width:3px
    style furo stroke-width:3px
    style myst_parser stroke-width:3px
    style platformdirs stroke-width:3px
    style pytest stroke-width:3px
    style pytest_cov stroke-width:3px
    style pytest_github_actions_annotate_failures stroke-width:3px
    style pytest_randomly stroke-width:3px
    style pytest_xdist stroke-width:3px
    style sphinx stroke-width:3px
    style sphinx_autodoc_typehints stroke-width:3px
    style sphinx_copybutton stroke-width:3px
    style sphinx_design stroke-width:3px
    style sphinxcontrib_mermaid stroke-width:3px
    style sphinxext_opengraph stroke-width:3px
    style types_boltons stroke-width:3px
    style whenever stroke-width:3px

    style primary-deps fill:#1565C020,stroke:#42A5F5
    style grp_docs fill:#546E7A20,stroke:#90A4AE
    style grp_test fill:#546E7A20,stroke:#90A4AE
    style grp_typing fill:#546E7A20,stroke:#90A4AE