about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/setuptools/_distutils
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/setuptools/_distutils
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/setuptools/_distutils')
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/__init__.py14
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/_log.py3
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/_macos_compat.py12
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/_modified.py95
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/_msvccompiler.py5
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/archive_util.py294
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/ccompiler.py26
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/cmd.py554
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/__init__.py23
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/_framework_compat.py54
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist.py167
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist_dumb.py141
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist_rpm.py598
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build.py156
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_clib.py201
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_ext.py812
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_py.py407
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_scripts.py160
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/check.py152
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/clean.py77
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/config.py360
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install.py805
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_data.py94
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_egg_info.py91
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_headers.py46
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_lib.py238
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_scripts.py62
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/command/sdist.py521
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/__init__.py18
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/numpy.py2
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/py39.py66
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/base.py1394
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/cygwin.py340
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/errors.py24
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/msvc.py614
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_base.py83
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_cygwin.py76
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_mingw.py48
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_msvc.py136
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_unix.py350
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/unix.py423
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/zos.py230
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/core.py289
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/cygwinccompiler.py31
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/debug.py5
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/dep_util.py14
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/dir_util.py244
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/dist.py1386
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/errors.py108
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/extension.py258
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/fancy_getopt.py471
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/file_util.py236
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/filelist.py431
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/log.py56
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/spawn.py134
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/sysconfig.py598
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/__init__.py42
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/compat/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/compat/py39.py40
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/support.py134
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_archive_util.py353
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist.py47
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist_dumb.py78
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist_rpm.py127
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build.py49
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_clib.py134
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_ext.py628
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_py.py196
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_scripts.py96
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_check.py194
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_clean.py45
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_cmd.py107
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_config_cmd.py87
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_core.py130
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_dir_util.py139
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_dist.py552
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_extension.py117
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_file_util.py95
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_filelist.py336
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install.py245
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_data.py74
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_headers.py33
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_lib.py110
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_scripts.py52
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_log.py12
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_modified.py126
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_sdist.py470
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_spawn.py141
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_sysconfig.py319
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_text_file.py127
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_util.py243
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_version.py80
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_versionpredicate.py0
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/unix_compat.py17
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/text_file.py286
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/unixccompiler.py9
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/util.py518
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/version.py348
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/versionpredicate.py175
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/_distutils/zosccompiler.py3
100 files changed, 21347 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/__init__.py
new file mode 100644
index 00000000..e374d5c5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/__init__.py
@@ -0,0 +1,14 @@
+import importlib
+import sys
+
+__version__, _, _ = sys.version.partition(' ')
+
+
+try:
+    # Allow Debian and pkgsrc (only) to customize system
+    # behavior. Ref pypa/distutils#2 and pypa/distutils#16.
+    # This hook is deprecated and no other environments
+    # should use it.
+    importlib.import_module('_distutils_system_mod')
+except ImportError:
+    pass
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/_log.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/_log.py
new file mode 100644
index 00000000..0148f157
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/_log.py
@@ -0,0 +1,3 @@
+import logging
+
+log = logging.getLogger()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/_macos_compat.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/_macos_compat.py
new file mode 100644
index 00000000..76ecb96a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/_macos_compat.py
@@ -0,0 +1,12 @@
+import importlib
+import sys
+
+
+def bypass_compiler_fixup(cmd, args):
+    return cmd
+
+
+if sys.platform == 'darwin':
+    compiler_fixup = importlib.import_module('_osx_support').compiler_fixup
+else:
+    compiler_fixup = bypass_compiler_fixup
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/_modified.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/_modified.py
new file mode 100644
index 00000000..f64cab7d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/_modified.py
@@ -0,0 +1,95 @@
+"""Timestamp comparison of files and groups of files."""
+
+from __future__ import annotations
+
+import functools
+import os.path
+from collections.abc import Callable, Iterable
+from typing import Literal, TypeVar
+
+from jaraco.functools import splat
+
+from .compat.py39 import zip_strict
+from .errors import DistutilsFileError
+
+_SourcesT = TypeVar(
+    "_SourcesT", bound="str | bytes | os.PathLike[str] | os.PathLike[bytes]"
+)
+_TargetsT = TypeVar(
+    "_TargetsT", bound="str | bytes | os.PathLike[str] | os.PathLike[bytes]"
+)
+
+
+def _newer(source, target):
+    return not os.path.exists(target) or (
+        os.path.getmtime(source) > os.path.getmtime(target)
+    )
+
+
+def newer(
+    source: str | bytes | os.PathLike[str] | os.PathLike[bytes],
+    target: str | bytes | os.PathLike[str] | os.PathLike[bytes],
+) -> bool:
+    """
+    Is source modified more recently than target.
+
+    Returns True if 'source' is modified more recently than
+    'target' or if 'target' does not exist.
+
+    Raises DistutilsFileError if 'source' does not exist.
+    """
+    if not os.path.exists(source):
+        raise DistutilsFileError(f"file {os.path.abspath(source)!r} does not exist")
+
+    return _newer(source, target)
+
+
+def newer_pairwise(
+    sources: Iterable[_SourcesT],
+    targets: Iterable[_TargetsT],
+    newer: Callable[[_SourcesT, _TargetsT], bool] = newer,
+) -> tuple[list[_SourcesT], list[_TargetsT]]:
+    """
+    Filter filenames where sources are newer than targets.
+
+    Walk two filename iterables in parallel, testing if each source is newer
+    than its corresponding target.  Returns a pair of lists (sources,
+    targets) where source is newer than target, according to the semantics
+    of 'newer()'.
+    """
+    newer_pairs = filter(splat(newer), zip_strict(sources, targets))
+    return tuple(map(list, zip(*newer_pairs))) or ([], [])
+
+
+def newer_group(
+    sources: Iterable[str | bytes | os.PathLike[str] | os.PathLike[bytes]],
+    target: str | bytes | os.PathLike[str] | os.PathLike[bytes],
+    missing: Literal["error", "ignore", "newer"] = "error",
+) -> bool:
+    """
+    Is target out-of-date with respect to any file in sources.
+
+    Return True if 'target' is out-of-date with respect to any file
+    listed in 'sources'. In other words, if 'target' exists and is newer
+    than every file in 'sources', return False; otherwise return True.
+    ``missing`` controls how to handle a missing source file:
+
+    - error (default): allow the ``stat()`` call to fail.
+    - ignore: silently disregard any missing source files.
+    - newer: treat missing source files as "target out of date". This
+      mode is handy in "dry-run" mode: it will pretend to carry out
+      commands that wouldn't work because inputs are missing, but
+      that doesn't matter because dry-run won't run the commands.
+    """
+
+    def missing_as_newer(source):
+        return missing == 'newer' and not os.path.exists(source)
+
+    ignored = os.path.exists if missing == 'ignore' else None
+    return not os.path.exists(target) or any(
+        missing_as_newer(source) or _newer(source, target)
+        for source in filter(ignored, sources)
+    )
+
+
+newer_pairwise_group = functools.partial(newer_pairwise, newer=newer_group)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/_msvccompiler.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/_msvccompiler.py
new file mode 100644
index 00000000..8471ccab
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/_msvccompiler.py
@@ -0,0 +1,5 @@
+from .compilers.C import msvc
+
+__all__ = ["MSVCCompiler"]
+
+MSVCCompiler = msvc.Compiler
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/archive_util.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/archive_util.py
new file mode 100644
index 00000000..d860f552
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/archive_util.py
@@ -0,0 +1,294 @@
+"""distutils.archive_util
+
+Utility functions for creating archive files (tarballs, zip files,
+that sort of thing)."""
+
+from __future__ import annotations
+
+import os
+from typing import Literal, overload
+
+try:
+    import zipfile
+except ImportError:
+    zipfile = None
+
+
+from ._log import log
+from .dir_util import mkpath
+from .errors import DistutilsExecError
+from .spawn import spawn
+
+try:
+    from pwd import getpwnam
+except ImportError:
+    getpwnam = None
+
+try:
+    from grp import getgrnam
+except ImportError:
+    getgrnam = None
+
+
+def _get_gid(name):
+    """Returns a gid, given a group name."""
+    if getgrnam is None or name is None:
+        return None
+    try:
+        result = getgrnam(name)
+    except KeyError:
+        result = None
+    if result is not None:
+        return result[2]
+    return None
+
+
+def _get_uid(name):
+    """Returns an uid, given a user name."""
+    if getpwnam is None or name is None:
+        return None
+    try:
+        result = getpwnam(name)
+    except KeyError:
+        result = None
+    if result is not None:
+        return result[2]
+    return None
+
+
+def make_tarball(
+    base_name: str,
+    base_dir: str | os.PathLike[str],
+    compress: Literal["gzip", "bzip2", "xz"] | None = "gzip",
+    verbose: bool = False,
+    dry_run: bool = False,
+    owner: str | None = None,
+    group: str | None = None,
+) -> str:
+    """Create a (possibly compressed) tar file from all the files under
+    'base_dir'.
+
+    'compress' must be "gzip" (the default), "bzip2", "xz", or None.
+
+    'owner' and 'group' can be used to define an owner and a group for the
+    archive that is being built. If not provided, the current owner and group
+    will be used.
+
+    The output tar file will be named 'base_dir' +  ".tar", possibly plus
+    the appropriate compression extension (".gz", ".bz2", ".xz" or ".Z").
+
+    Returns the output filename.
+    """
+    tar_compression = {
+        'gzip': 'gz',
+        'bzip2': 'bz2',
+        'xz': 'xz',
+        None: '',
+    }
+    compress_ext = {'gzip': '.gz', 'bzip2': '.bz2', 'xz': '.xz'}
+
+    # flags for compression program, each element of list will be an argument
+    if compress is not None and compress not in compress_ext.keys():
+        raise ValueError(
+            "bad value for 'compress': must be None, 'gzip', 'bzip2', 'xz'"
+        )
+
+    archive_name = base_name + '.tar'
+    archive_name += compress_ext.get(compress, '')
+
+    mkpath(os.path.dirname(archive_name), dry_run=dry_run)
+
+    # creating the tarball
+    import tarfile  # late import so Python build itself doesn't break
+
+    log.info('Creating tar archive')
+
+    uid = _get_uid(owner)
+    gid = _get_gid(group)
+
+    def _set_uid_gid(tarinfo):
+        if gid is not None:
+            tarinfo.gid = gid
+            tarinfo.gname = group
+        if uid is not None:
+            tarinfo.uid = uid
+            tarinfo.uname = owner
+        return tarinfo
+
+    if not dry_run:
+        tar = tarfile.open(archive_name, f'w|{tar_compression[compress]}')
+        try:
+            tar.add(base_dir, filter=_set_uid_gid)
+        finally:
+            tar.close()
+
+    return archive_name
+
+
+def make_zipfile(  # noqa: C901
+    base_name: str,
+    base_dir: str | os.PathLike[str],
+    verbose: bool = False,
+    dry_run: bool = False,
+) -> str:
+    """Create a zip file from all the files under 'base_dir'.
+
+    The output zip file will be named 'base_name' + ".zip".  Uses either the
+    "zipfile" Python module (if available) or the InfoZIP "zip" utility
+    (if installed and found on the default search path).  If neither tool is
+    available, raises DistutilsExecError.  Returns the name of the output zip
+    file.
+    """
+    zip_filename = base_name + ".zip"
+    mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
+
+    # If zipfile module is not available, try spawning an external
+    # 'zip' command.
+    if zipfile is None:
+        if verbose:
+            zipoptions = "-r"
+        else:
+            zipoptions = "-rq"
+
+        try:
+            spawn(["zip", zipoptions, zip_filename, base_dir], dry_run=dry_run)
+        except DistutilsExecError:
+            # XXX really should distinguish between "couldn't find
+            # external 'zip' command" and "zip failed".
+            raise DistutilsExecError(
+                f"unable to create zip file '{zip_filename}': "
+                "could neither import the 'zipfile' module nor "
+                "find a standalone zip utility"
+            )
+
+    else:
+        log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
+
+        if not dry_run:
+            try:
+                zip = zipfile.ZipFile(
+                    zip_filename, "w", compression=zipfile.ZIP_DEFLATED
+                )
+            except RuntimeError:
+                zip = zipfile.ZipFile(zip_filename, "w", compression=zipfile.ZIP_STORED)
+
+            with zip:
+                if base_dir != os.curdir:
+                    path = os.path.normpath(os.path.join(base_dir, ''))
+                    zip.write(path, path)
+                    log.info("adding '%s'", path)
+                for dirpath, dirnames, filenames in os.walk(base_dir):
+                    for name in dirnames:
+                        path = os.path.normpath(os.path.join(dirpath, name, ''))
+                        zip.write(path, path)
+                        log.info("adding '%s'", path)
+                    for name in filenames:
+                        path = os.path.normpath(os.path.join(dirpath, name))
+                        if os.path.isfile(path):
+                            zip.write(path, path)
+                            log.info("adding '%s'", path)
+
+    return zip_filename
+
+
+ARCHIVE_FORMATS = {
+    'gztar': (make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"),
+    'bztar': (make_tarball, [('compress', 'bzip2')], "bzip2'ed tar-file"),
+    'xztar': (make_tarball, [('compress', 'xz')], "xz'ed tar-file"),
+    'ztar': (make_tarball, [('compress', 'compress')], "compressed tar file"),
+    'tar': (make_tarball, [('compress', None)], "uncompressed tar file"),
+    'zip': (make_zipfile, [], "ZIP file"),
+}
+
+
+def check_archive_formats(formats):
+    """Returns the first format from the 'format' list that is unknown.
+
+    If all formats are known, returns None
+    """
+    for format in formats:
+        if format not in ARCHIVE_FORMATS:
+            return format
+    return None
+
+
+@overload
+def make_archive(
+    base_name: str,
+    format: str,
+    root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
+    base_dir: str | None = None,
+    verbose: bool = False,
+    dry_run: bool = False,
+    owner: str | None = None,
+    group: str | None = None,
+) -> str: ...
+@overload
+def make_archive(
+    base_name: str | os.PathLike[str],
+    format: str,
+    root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes],
+    base_dir: str | None = None,
+    verbose: bool = False,
+    dry_run: bool = False,
+    owner: str | None = None,
+    group: str | None = None,
+) -> str: ...
+def make_archive(
+    base_name: str | os.PathLike[str],
+    format: str,
+    root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
+    base_dir: str | None = None,
+    verbose: bool = False,
+    dry_run: bool = False,
+    owner: str | None = None,
+    group: str | None = None,
+) -> str:
+    """Create an archive file (eg. zip or tar).
+
+    'base_name' is the name of the file to create, minus any format-specific
+    extension; 'format' is the archive format: one of "zip", "tar", "gztar",
+    "bztar", "xztar", or "ztar".
+
+    'root_dir' is a directory that will be the root directory of the
+    archive; ie. we typically chdir into 'root_dir' before creating the
+    archive.  'base_dir' is the directory where we start archiving from;
+    ie. 'base_dir' will be the common prefix of all files and
+    directories in the archive.  'root_dir' and 'base_dir' both default
+    to the current directory.  Returns the name of the archive file.
+
+    'owner' and 'group' are used when creating a tar archive. By default,
+    uses the current owner and group.
+    """
+    save_cwd = os.getcwd()
+    if root_dir is not None:
+        log.debug("changing into '%s'", root_dir)
+        base_name = os.path.abspath(base_name)
+        if not dry_run:
+            os.chdir(root_dir)
+
+    if base_dir is None:
+        base_dir = os.curdir
+
+    kwargs = {'dry_run': dry_run}
+
+    try:
+        format_info = ARCHIVE_FORMATS[format]
+    except KeyError:
+        raise ValueError(f"unknown archive format '{format}'")
+
+    func = format_info[0]
+    kwargs.update(format_info[1])
+
+    if format != 'zip':
+        kwargs['owner'] = owner
+        kwargs['group'] = group
+
+    try:
+        filename = func(base_name, base_dir, **kwargs)
+    finally:
+        if root_dir is not None:
+            log.debug("changing back to '%s'", save_cwd)
+            os.chdir(save_cwd)
+
+    return filename
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/ccompiler.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/ccompiler.py
new file mode 100644
index 00000000..58bc6a55
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/ccompiler.py
@@ -0,0 +1,26 @@
+from .compat.numpy import (  # noqa: F401
+    _default_compilers,
+    compiler_class,
+)
+from .compilers.C import base
+from .compilers.C.base import (
+    gen_lib_options,
+    gen_preprocess_options,
+    get_default_compiler,
+    new_compiler,
+    show_compilers,
+)
+from .compilers.C.errors import CompileError, LinkError
+
+__all__ = [
+    'CompileError',
+    'LinkError',
+    'gen_lib_options',
+    'gen_preprocess_options',
+    'get_default_compiler',
+    'new_compiler',
+    'show_compilers',
+]
+
+
+CCompiler = base.Compiler
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/cmd.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/cmd.py
new file mode 100644
index 00000000..241621bd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/cmd.py
@@ -0,0 +1,554 @@
+"""distutils.cmd
+
+Provides the Command class, the base class for the command classes
+in the distutils.command package.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+import re
+import sys
+from abc import abstractmethod
+from collections.abc import Callable, MutableSequence
+from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload
+
+from . import _modified, archive_util, dir_util, file_util, util
+from ._log import log
+from .errors import DistutilsOptionError
+
+if TYPE_CHECKING:
+    # type-only import because of mutual dependence between these classes
+    from distutils.dist import Distribution
+
+    from typing_extensions import TypeVarTuple, Unpack
+
+    _Ts = TypeVarTuple("_Ts")
+
+_StrPathT = TypeVar("_StrPathT", bound="str | os.PathLike[str]")
+_BytesPathT = TypeVar("_BytesPathT", bound="bytes | os.PathLike[bytes]")
+_CommandT = TypeVar("_CommandT", bound="Command")
+
+
+class Command:
+    """Abstract base class for defining command classes, the "worker bees"
+    of the Distutils.  A useful analogy for command classes is to think of
+    them as subroutines with local variables called "options".  The options
+    are "declared" in 'initialize_options()' and "defined" (given their
+    final values, aka "finalized") in 'finalize_options()', both of which
+    must be defined by every command class.  The distinction between the
+    two is necessary because option values might come from the outside
+    world (command line, config file, ...), and any options dependent on
+    other options must be computed *after* these outside influences have
+    been processed -- hence 'finalize_options()'.  The "body" of the
+    subroutine, where it does all its work based on the values of its
+    options, is the 'run()' method, which must also be implemented by every
+    command class.
+    """
+
+    # 'sub_commands' formalizes the notion of a "family" of commands,
+    # eg. "install" as the parent with sub-commands "install_lib",
+    # "install_headers", etc.  The parent of a family of commands
+    # defines 'sub_commands' as a class attribute; it's a list of
+    #    (command_name : string, predicate : unbound_method | string | None)
+    # tuples, where 'predicate' is a method of the parent command that
+    # determines whether the corresponding command is applicable in the
+    # current situation.  (Eg. we "install_headers" is only applicable if
+    # we have any C header files to install.)  If 'predicate' is None,
+    # that command is always applicable.
+    #
+    # 'sub_commands' is usually defined at the *end* of a class, because
+    # predicates can be unbound methods, so they must already have been
+    # defined.  The canonical example is the "install" command.
+    sub_commands: ClassVar[  # Any to work around variance issues
+        list[tuple[str, Callable[[Any], bool] | None]]
+    ] = []
+
+    user_options: ClassVar[
+        # Specifying both because list is invariant. Avoids mypy override assignment issues
+        list[tuple[str, str, str]] | list[tuple[str, str | None, str]]
+    ] = []
+
+    # -- Creation/initialization methods -------------------------------
+
+    def __init__(self, dist: Distribution) -> None:
+        """Create and initialize a new Command object.  Most importantly,
+        invokes the 'initialize_options()' method, which is the real
+        initializer and depends on the actual command being
+        instantiated.
+        """
+        # late import because of mutual dependence between these classes
+        from distutils.dist import Distribution
+
+        if not isinstance(dist, Distribution):
+            raise TypeError("dist must be a Distribution instance")
+        if self.__class__ is Command:
+            raise RuntimeError("Command is an abstract class")
+
+        self.distribution = dist
+        self.initialize_options()
+
+        # Per-command versions of the global flags, so that the user can
+        # customize Distutils' behaviour command-by-command and let some
+        # commands fall back on the Distribution's behaviour.  None means
+        # "not defined, check self.distribution's copy", while 0 or 1 mean
+        # false and true (duh).  Note that this means figuring out the real
+        # value of each flag is a touch complicated -- hence "self._dry_run"
+        # will be handled by __getattr__, below.
+        # XXX This needs to be fixed.
+        self._dry_run = None
+
+        # verbose is largely ignored, but needs to be set for
+        # backwards compatibility (I think)?
+        self.verbose = dist.verbose
+
+        # Some commands define a 'self.force' option to ignore file
+        # timestamps, but methods defined *here* assume that
+        # 'self.force' exists for all commands.  So define it here
+        # just to be safe.
+        self.force = None
+
+        # The 'help' flag is just used for command-line parsing, so
+        # none of that complicated bureaucracy is needed.
+        self.help = False
+
+        # 'finalized' records whether or not 'finalize_options()' has been
+        # called.  'finalize_options()' itself should not pay attention to
+        # this flag: it is the business of 'ensure_finalized()', which
+        # always calls 'finalize_options()', to respect/update it.
+        self.finalized = False
+
+    # XXX A more explicit way to customize dry_run would be better.
+    def __getattr__(self, attr):
+        if attr == 'dry_run':
+            myval = getattr(self, "_" + attr)
+            if myval is None:
+                return getattr(self.distribution, attr)
+            else:
+                return myval
+        else:
+            raise AttributeError(attr)
+
+    def ensure_finalized(self) -> None:
+        if not self.finalized:
+            self.finalize_options()
+        self.finalized = True
+
+    # Subclasses must define:
+    #   initialize_options()
+    #     provide default values for all options; may be customized by
+    #     setup script, by options from config file(s), or by command-line
+    #     options
+    #   finalize_options()
+    #     decide on the final values for all options; this is called
+    #     after all possible intervention from the outside world
+    #     (command-line, option file, etc.) has been processed
+    #   run()
+    #     run the command: do whatever it is we're here to do,
+    #     controlled by the command's various option values
+
+    @abstractmethod
+    def initialize_options(self) -> None:
+        """Set default values for all the options that this command
+        supports.  Note that these defaults may be overridden by other
+        commands, by the setup script, by config files, or by the
+        command-line.  Thus, this is not the place to code dependencies
+        between options; generally, 'initialize_options()' implementations
+        are just a bunch of "self.foo = None" assignments.
+
+        This method must be implemented by all command classes.
+        """
+        raise RuntimeError(
+            f"abstract method -- subclass {self.__class__} must override"
+        )
+
+    @abstractmethod
+    def finalize_options(self) -> None:
+        """Set final values for all the options that this command supports.
+        This is always called as late as possible, ie.  after any option
+        assignments from the command-line or from other commands have been
+        done.  Thus, this is the place to code option dependencies: if
+        'foo' depends on 'bar', then it is safe to set 'foo' from 'bar' as
+        long as 'foo' still has the same value it was assigned in
+        'initialize_options()'.
+
+        This method must be implemented by all command classes.
+        """
+        raise RuntimeError(
+            f"abstract method -- subclass {self.__class__} must override"
+        )
+
+    def dump_options(self, header=None, indent=""):
+        from distutils.fancy_getopt import longopt_xlate
+
+        if header is None:
+            header = f"command options for '{self.get_command_name()}':"
+        self.announce(indent + header, level=logging.INFO)
+        indent = indent + "  "
+        for option, _, _ in self.user_options:
+            option = option.translate(longopt_xlate)
+            if option[-1] == "=":
+                option = option[:-1]
+            value = getattr(self, option)
+            self.announce(indent + f"{option} = {value}", level=logging.INFO)
+
+    @abstractmethod
+    def run(self) -> None:
+        """A command's raison d'etre: carry out the action it exists to
+        perform, controlled by the options initialized in
+        'initialize_options()', customized by other commands, the setup
+        script, the command-line, and config files, and finalized in
+        'finalize_options()'.  All terminal output and filesystem
+        interaction should be done by 'run()'.
+
+        This method must be implemented by all command classes.
+        """
+        raise RuntimeError(
+            f"abstract method -- subclass {self.__class__} must override"
+        )
+
+    def announce(self, msg: object, level: int = logging.DEBUG) -> None:
+        log.log(level, msg)
+
+    def debug_print(self, msg: object) -> None:
+        """Print 'msg' to stdout if the global DEBUG (taken from the
+        DISTUTILS_DEBUG environment variable) flag is true.
+        """
+        from distutils.debug import DEBUG
+
+        if DEBUG:
+            print(msg)
+            sys.stdout.flush()
+
+    # -- Option validation methods -------------------------------------
+    # (these are very handy in writing the 'finalize_options()' method)
+    #
+    # NB. the general philosophy here is to ensure that a particular option
+    # value meets certain type and value constraints.  If not, we try to
+    # force it into conformance (eg. if we expect a list but have a string,
+    # split the string on comma and/or whitespace).  If we can't force the
+    # option into conformance, raise DistutilsOptionError.  Thus, command
+    # classes need do nothing more than (eg.)
+    #   self.ensure_string_list('foo')
+    # and they can be guaranteed that thereafter, self.foo will be
+    # a list of strings.
+
+    def _ensure_stringlike(self, option, what, default=None):
+        val = getattr(self, option)
+        if val is None:
+            setattr(self, option, default)
+            return default
+        elif not isinstance(val, str):
+            raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)")
+        return val
+
+    def ensure_string(self, option: str, default: str | None = None) -> None:
+        """Ensure that 'option' is a string; if not defined, set it to
+        'default'.
+        """
+        self._ensure_stringlike(option, "string", default)
+
+    def ensure_string_list(self, option: str) -> None:
+        r"""Ensure that 'option' is a list of strings.  If 'option' is
+        currently a string, we split it either on /,\s*/ or /\s+/, so
+        "foo bar baz", "foo,bar,baz", and "foo,   bar baz" all become
+        ["foo", "bar", "baz"].
+        """
+        val = getattr(self, option)
+        if val is None:
+            return
+        elif isinstance(val, str):
+            setattr(self, option, re.split(r',\s*|\s+', val))
+        else:
+            if isinstance(val, list):
+                ok = all(isinstance(v, str) for v in val)
+            else:
+                ok = False
+            if not ok:
+                raise DistutilsOptionError(
+                    f"'{option}' must be a list of strings (got {val!r})"
+                )
+
+    def _ensure_tested_string(self, option, tester, what, error_fmt, default=None):
+        val = self._ensure_stringlike(option, what, default)
+        if val is not None and not tester(val):
+            raise DistutilsOptionError(
+                ("error in '%s' option: " + error_fmt) % (option, val)
+            )
+
+    def ensure_filename(self, option: str) -> None:
+        """Ensure that 'option' is the name of an existing file."""
+        self._ensure_tested_string(
+            option, os.path.isfile, "filename", "'%s' does not exist or is not a file"
+        )
+
+    def ensure_dirname(self, option: str) -> None:
+        self._ensure_tested_string(
+            option,
+            os.path.isdir,
+            "directory name",
+            "'%s' does not exist or is not a directory",
+        )
+
+    # -- Convenience methods for commands ------------------------------
+
+    def get_command_name(self) -> str:
+        if hasattr(self, 'command_name'):
+            return self.command_name
+        else:
+            return self.__class__.__name__
+
+    def set_undefined_options(
+        self, src_cmd: str, *option_pairs: tuple[str, str]
+    ) -> None:
+        """Set the values of any "undefined" options from corresponding
+        option values in some other command object.  "Undefined" here means
+        "is None", which is the convention used to indicate that an option
+        has not been changed between 'initialize_options()' and
+        'finalize_options()'.  Usually called from 'finalize_options()' for
+        options that depend on some other command rather than another
+        option of the same command.  'src_cmd' is the other command from
+        which option values will be taken (a command object will be created
+        for it if necessary); the remaining arguments are
+        '(src_option,dst_option)' tuples which mean "take the value of
+        'src_option' in the 'src_cmd' command object, and copy it to
+        'dst_option' in the current command object".
+        """
+        # Option_pairs: list of (src_option, dst_option) tuples
+        src_cmd_obj = self.distribution.get_command_obj(src_cmd)
+        src_cmd_obj.ensure_finalized()
+        for src_option, dst_option in option_pairs:
+            if getattr(self, dst_option) is None:
+                setattr(self, dst_option, getattr(src_cmd_obj, src_option))
+
+    # NOTE: Because distutils is private to Setuptools and not all commands are exposed here,
+    # not every possible command is enumerated in the signature.
+    def get_finalized_command(self, command: str, create: bool = True) -> Command:
+        """Wrapper around Distribution's 'get_command_obj()' method: find
+        (create if necessary and 'create' is true) the command object for
+        'command', call its 'ensure_finalized()' method, and return the
+        finalized command object.
+        """
+        cmd_obj = self.distribution.get_command_obj(command, create)
+        cmd_obj.ensure_finalized()
+        return cmd_obj
+
+    # XXX rename to 'get_reinitialized_command()'? (should do the
+    # same in dist.py, if so)
+    @overload
+    def reinitialize_command(
+        self, command: str, reinit_subcommands: bool = False
+    ) -> Command: ...
+    @overload
+    def reinitialize_command(
+        self, command: _CommandT, reinit_subcommands: bool = False
+    ) -> _CommandT: ...
+    def reinitialize_command(
+        self, command: str | Command, reinit_subcommands=False
+    ) -> Command:
+        return self.distribution.reinitialize_command(command, reinit_subcommands)
+
+    def run_command(self, command: str) -> None:
+        """Run some other command: uses the 'run_command()' method of
+        Distribution, which creates and finalizes the command object if
+        necessary and then invokes its 'run()' method.
+        """
+        self.distribution.run_command(command)
+
+    def get_sub_commands(self) -> list[str]:
+        """Determine the sub-commands that are relevant in the current
+        distribution (ie., that need to be run).  This is based on the
+        'sub_commands' class attribute: each tuple in that list may include
+        a method that we call to determine if the subcommand needs to be
+        run for the current distribution.  Return a list of command names.
+        """
+        commands = []
+        for cmd_name, method in self.sub_commands:
+            if method is None or method(self):
+                commands.append(cmd_name)
+        return commands
+
+    # -- External world manipulation -----------------------------------
+
+    def warn(self, msg: object) -> None:
+        log.warning("warning: %s: %s\n", self.get_command_name(), msg)
+
+    def execute(
+        self,
+        func: Callable[[Unpack[_Ts]], object],
+        args: tuple[Unpack[_Ts]],
+        msg: object = None,
+        level: int = 1,
+    ) -> None:
+        util.execute(func, args, msg, dry_run=self.dry_run)
+
+    def mkpath(self, name: str, mode: int = 0o777) -> None:
+        dir_util.mkpath(name, mode, dry_run=self.dry_run)
+
+    @overload
+    def copy_file(
+        self,
+        infile: str | os.PathLike[str],
+        outfile: _StrPathT,
+        preserve_mode: bool = True,
+        preserve_times: bool = True,
+        link: str | None = None,
+        level: int = 1,
+    ) -> tuple[_StrPathT | str, bool]: ...
+    @overload
+    def copy_file(
+        self,
+        infile: bytes | os.PathLike[bytes],
+        outfile: _BytesPathT,
+        preserve_mode: bool = True,
+        preserve_times: bool = True,
+        link: str | None = None,
+        level: int = 1,
+    ) -> tuple[_BytesPathT | bytes, bool]: ...
+    def copy_file(
+        self,
+        infile: str | os.PathLike[str] | bytes | os.PathLike[bytes],
+        outfile: str | os.PathLike[str] | bytes | os.PathLike[bytes],
+        preserve_mode: bool = True,
+        preserve_times: bool = True,
+        link: str | None = None,
+        level: int = 1,
+    ) -> tuple[str | os.PathLike[str] | bytes | os.PathLike[bytes], bool]:
+        """Copy a file respecting verbose, dry-run and force flags.  (The
+        former two default to whatever is in the Distribution object, and
+        the latter defaults to false for commands that don't define it.)"""
+        return file_util.copy_file(
+            infile,
+            outfile,
+            preserve_mode,
+            preserve_times,
+            not self.force,
+            link,
+            dry_run=self.dry_run,
+        )
+
+    def copy_tree(
+        self,
+        infile: str | os.PathLike[str],
+        outfile: str,
+        preserve_mode: bool = True,
+        preserve_times: bool = True,
+        preserve_symlinks: bool = False,
+        level: int = 1,
+    ) -> list[str]:
+        """Copy an entire directory tree respecting verbose, dry-run,
+        and force flags.
+        """
+        return dir_util.copy_tree(
+            infile,
+            outfile,
+            preserve_mode,
+            preserve_times,
+            preserve_symlinks,
+            not self.force,
+            dry_run=self.dry_run,
+        )
+
+    @overload
+    def move_file(
+        self, src: str | os.PathLike[str], dst: _StrPathT, level: int = 1
+    ) -> _StrPathT | str: ...
+    @overload
+    def move_file(
+        self, src: bytes | os.PathLike[bytes], dst: _BytesPathT, level: int = 1
+    ) -> _BytesPathT | bytes: ...
+    def move_file(
+        self,
+        src: str | os.PathLike[str] | bytes | os.PathLike[bytes],
+        dst: str | os.PathLike[str] | bytes | os.PathLike[bytes],
+        level: int = 1,
+    ) -> str | os.PathLike[str] | bytes | os.PathLike[bytes]:
+        """Move a file respecting dry-run flag."""
+        return file_util.move_file(src, dst, dry_run=self.dry_run)
+
+    def spawn(
+        self, cmd: MutableSequence[str], search_path: bool = True, level: int = 1
+    ) -> None:
+        """Spawn an external command respecting dry-run flag."""
+        from distutils.spawn import spawn
+
+        spawn(cmd, search_path, dry_run=self.dry_run)
+
+    @overload
+    def make_archive(
+        self,
+        base_name: str,
+        format: str,
+        root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
+        base_dir: str | None = None,
+        owner: str | None = None,
+        group: str | None = None,
+    ) -> str: ...
+    @overload
+    def make_archive(
+        self,
+        base_name: str | os.PathLike[str],
+        format: str,
+        root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes],
+        base_dir: str | None = None,
+        owner: str | None = None,
+        group: str | None = None,
+    ) -> str: ...
+    def make_archive(
+        self,
+        base_name: str | os.PathLike[str],
+        format: str,
+        root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
+        base_dir: str | None = None,
+        owner: str | None = None,
+        group: str | None = None,
+    ) -> str:
+        return archive_util.make_archive(
+            base_name,
+            format,
+            root_dir,
+            base_dir,
+            dry_run=self.dry_run,
+            owner=owner,
+            group=group,
+        )
+
+    def make_file(
+        self,
+        infiles: str | list[str] | tuple[str, ...],
+        outfile: str | os.PathLike[str] | bytes | os.PathLike[bytes],
+        func: Callable[[Unpack[_Ts]], object],
+        args: tuple[Unpack[_Ts]],
+        exec_msg: object = None,
+        skip_msg: object = None,
+        level: int = 1,
+    ) -> None:
+        """Special case of 'execute()' for operations that process one or
+        more input files and generate one output file.  Works just like
+        'execute()', except the operation is skipped and a different
+        message printed if 'outfile' already exists and is newer than all
+        files listed in 'infiles'.  If the command defined 'self.force',
+        and it is true, then the command is unconditionally run -- does no
+        timestamp checks.
+        """
+        if skip_msg is None:
+            skip_msg = f"skipping {outfile} (inputs unchanged)"
+
+        # Allow 'infiles' to be a single string
+        if isinstance(infiles, str):
+            infiles = (infiles,)
+        elif not isinstance(infiles, (list, tuple)):
+            raise TypeError("'infiles' must be a string, or a list or tuple of strings")
+
+        if exec_msg is None:
+            exec_msg = "generating {} from {}".format(outfile, ', '.join(infiles))
+
+        # If 'outfile' must be regenerated (either because it doesn't
+        # exist, is out-of-date, or the 'force' flag is true) then
+        # perform the action that presumably regenerates it
+        if self.force or _modified.newer_group(infiles, outfile):
+            self.execute(func, args, exec_msg, level)
+        # Otherwise, print the "skip" message
+        else:
+            log.debug(skip_msg)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/__init__.py
new file mode 100644
index 00000000..0f8a1692
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/__init__.py
@@ -0,0 +1,23 @@
+"""distutils.command
+
+Package containing implementation of all the standard Distutils
+commands."""
+
+__all__ = [
+    'build',
+    'build_py',
+    'build_ext',
+    'build_clib',
+    'build_scripts',
+    'clean',
+    'install',
+    'install_lib',
+    'install_headers',
+    'install_scripts',
+    'install_data',
+    'sdist',
+    'bdist',
+    'bdist_dumb',
+    'bdist_rpm',
+    'check',
+]
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/_framework_compat.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/_framework_compat.py
new file mode 100644
index 00000000..00d34bc7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/_framework_compat.py
@@ -0,0 +1,54 @@
+"""
+Backward compatibility for homebrew builds on macOS.
+"""
+
+import functools
+import os
+import subprocess
+import sys
+import sysconfig
+
+
+@functools.lru_cache
+def enabled():
+    """
+    Only enabled for Python 3.9 framework homebrew builds
+    except ensurepip and venv.
+    """
+    PY39 = (3, 9) < sys.version_info < (3, 10)
+    framework = sys.platform == 'darwin' and sys._framework
+    homebrew = "Cellar" in sysconfig.get_config_var('projectbase')
+    venv = sys.prefix != sys.base_prefix
+    ensurepip = os.environ.get("ENSUREPIP_OPTIONS")
+    return PY39 and framework and homebrew and not venv and not ensurepip
+
+
+schemes = dict(
+    osx_framework_library=dict(
+        stdlib='{installed_base}/{platlibdir}/python{py_version_short}',
+        platstdlib='{platbase}/{platlibdir}/python{py_version_short}',
+        purelib='{homebrew_prefix}/lib/python{py_version_short}/site-packages',
+        platlib='{homebrew_prefix}/{platlibdir}/python{py_version_short}/site-packages',
+        include='{installed_base}/include/python{py_version_short}{abiflags}',
+        platinclude='{installed_platbase}/include/python{py_version_short}{abiflags}',
+        scripts='{homebrew_prefix}/bin',
+        data='{homebrew_prefix}',
+    )
+)
+
+
+@functools.lru_cache
+def vars():
+    if not enabled():
+        return {}
+    homebrew_prefix = subprocess.check_output(['brew', '--prefix'], text=True).strip()
+    return locals()
+
+
+def scheme(name):
+    """
+    Override the selected scheme for posix_prefix.
+    """
+    if not enabled() or not name.endswith('_prefix'):
+        return name
+    return 'osx_framework_library'
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist.py
new file mode 100644
index 00000000..07811aab
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist.py
@@ -0,0 +1,167 @@
+"""distutils.command.bdist
+
+Implements the Distutils 'bdist' command (create a built [binary]
+distribution)."""
+
+from __future__ import annotations
+
+import os
+import warnings
+from collections.abc import Callable
+from typing import TYPE_CHECKING, ClassVar
+
+from ..core import Command
+from ..errors import DistutilsOptionError, DistutilsPlatformError
+from ..util import get_platform
+
+if TYPE_CHECKING:
+    from typing_extensions import deprecated
+else:
+
+    def deprecated(message):
+        return lambda fn: fn
+
+
+def show_formats():
+    """Print list of available formats (arguments to "--format" option)."""
+    from ..fancy_getopt import FancyGetopt
+
+    formats = [
+        ("formats=" + format, None, bdist.format_commands[format][1])
+        for format in bdist.format_commands
+    ]
+    pretty_printer = FancyGetopt(formats)
+    pretty_printer.print_help("List of available distribution formats:")
+
+
+class ListCompat(dict[str, tuple[str, str]]):
+    # adapter to allow for Setuptools compatibility in format_commands
+    @deprecated("format_commands is now a dict. append is deprecated.")
+    def append(self, item: object) -> None:
+        warnings.warn(
+            "format_commands is now a dict. append is deprecated.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+
+
+class bdist(Command):
+    description = "create a built (binary) distribution"
+
+    user_options = [
+        ('bdist-base=', 'b', "temporary directory for creating built distributions"),
+        (
+            'plat-name=',
+            'p',
+            "platform name to embed in generated filenames "
+            f"[default: {get_platform()}]",
+        ),
+        ('formats=', None, "formats for distribution (comma-separated list)"),
+        (
+            'dist-dir=',
+            'd',
+            "directory to put final built distributions in [default: dist]",
+        ),
+        ('skip-build', None, "skip rebuilding everything (for testing/debugging)"),
+        (
+            'owner=',
+            'u',
+            "Owner name used when creating a tar file [default: current user]",
+        ),
+        (
+            'group=',
+            'g',
+            "Group name used when creating a tar file [default: current group]",
+        ),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['skip-build']
+
+    help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
+        ('help-formats', None, "lists available distribution formats", show_formats),
+    ]
+
+    # The following commands do not take a format option from bdist
+    no_format_option: ClassVar[tuple[str, ...]] = ('bdist_rpm',)
+
+    # This won't do in reality: will need to distinguish RPM-ish Linux,
+    # Debian-ish Linux, Solaris, FreeBSD, ..., Windows, Mac OS.
+    default_format: ClassVar[dict[str, str]] = {'posix': 'gztar', 'nt': 'zip'}
+
+    # Define commands in preferred order for the --help-formats option
+    format_commands = ListCompat({
+        'rpm': ('bdist_rpm', "RPM distribution"),
+        'gztar': ('bdist_dumb', "gzip'ed tar file"),
+        'bztar': ('bdist_dumb', "bzip2'ed tar file"),
+        'xztar': ('bdist_dumb', "xz'ed tar file"),
+        'ztar': ('bdist_dumb', "compressed tar file"),
+        'tar': ('bdist_dumb', "tar file"),
+        'zip': ('bdist_dumb', "ZIP file"),
+    })
+
+    # for compatibility until consumers only reference format_commands
+    format_command = format_commands
+
+    def initialize_options(self):
+        self.bdist_base = None
+        self.plat_name = None
+        self.formats = None
+        self.dist_dir = None
+        self.skip_build = False
+        self.group = None
+        self.owner = None
+
+    def finalize_options(self) -> None:
+        # have to finalize 'plat_name' before 'bdist_base'
+        if self.plat_name is None:
+            if self.skip_build:
+                self.plat_name = get_platform()
+            else:
+                self.plat_name = self.get_finalized_command('build').plat_name
+
+        # 'bdist_base' -- parent of per-built-distribution-format
+        # temporary directories (eg. we'll probably have
+        # "build/bdist.<plat>/dumb", "build/bdist.<plat>/rpm", etc.)
+        if self.bdist_base is None:
+            build_base = self.get_finalized_command('build').build_base
+            self.bdist_base = os.path.join(build_base, 'bdist.' + self.plat_name)
+
+        self.ensure_string_list('formats')
+        if self.formats is None:
+            try:
+                self.formats = [self.default_format[os.name]]
+            except KeyError:
+                raise DistutilsPlatformError(
+                    "don't know how to create built distributions "
+                    f"on platform {os.name}"
+                )
+
+        if self.dist_dir is None:
+            self.dist_dir = "dist"
+
+    def run(self) -> None:
+        # Figure out which sub-commands we need to run.
+        commands = []
+        for format in self.formats:
+            try:
+                commands.append(self.format_commands[format][0])
+            except KeyError:
+                raise DistutilsOptionError(f"invalid format '{format}'")
+
+        # Reinitialize and run each command.
+        for i in range(len(self.formats)):
+            cmd_name = commands[i]
+            sub_cmd = self.reinitialize_command(cmd_name)
+            if cmd_name not in self.no_format_option:
+                sub_cmd.format = self.formats[i]
+
+            # passing the owner and group names for tar archiving
+            if cmd_name == 'bdist_dumb':
+                sub_cmd.owner = self.owner
+                sub_cmd.group = self.group
+
+            # If we're going to need to run this command again, tell it to
+            # keep its temporary files around so subsequent runs go faster.
+            if cmd_name in commands[i + 1 :]:
+                sub_cmd.keep_temp = True
+            self.run_command(cmd_name)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist_dumb.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist_dumb.py
new file mode 100644
index 00000000..ccad66f4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist_dumb.py
@@ -0,0 +1,141 @@
+"""distutils.command.bdist_dumb
+
+Implements the Distutils 'bdist_dumb' command (create a "dumb" built
+distribution -- i.e., just an archive to be unpacked under $prefix or
+$exec_prefix)."""
+
+import os
+from distutils._log import log
+from typing import ClassVar
+
+from ..core import Command
+from ..dir_util import ensure_relative, remove_tree
+from ..errors import DistutilsPlatformError
+from ..sysconfig import get_python_version
+from ..util import get_platform
+
+
+class bdist_dumb(Command):
+    description = "create a \"dumb\" built distribution"
+
+    user_options = [
+        ('bdist-dir=', 'd', "temporary directory for creating the distribution"),
+        (
+            'plat-name=',
+            'p',
+            "platform name to embed in generated filenames "
+            f"[default: {get_platform()}]",
+        ),
+        (
+            'format=',
+            'f',
+            "archive format to create (tar, gztar, bztar, xztar, ztar, zip)",
+        ),
+        (
+            'keep-temp',
+            'k',
+            "keep the pseudo-installation tree around after creating the distribution archive",
+        ),
+        ('dist-dir=', 'd', "directory to put final built distributions in"),
+        ('skip-build', None, "skip rebuilding everything (for testing/debugging)"),
+        (
+            'relative',
+            None,
+            "build the archive using relative paths [default: false]",
+        ),
+        (
+            'owner=',
+            'u',
+            "Owner name used when creating a tar file [default: current user]",
+        ),
+        (
+            'group=',
+            'g',
+            "Group name used when creating a tar file [default: current group]",
+        ),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['keep-temp', 'skip-build', 'relative']
+
+    default_format = {'posix': 'gztar', 'nt': 'zip'}
+
+    def initialize_options(self):
+        self.bdist_dir = None
+        self.plat_name = None
+        self.format = None
+        self.keep_temp = False
+        self.dist_dir = None
+        self.skip_build = None
+        self.relative = False
+        self.owner = None
+        self.group = None
+
+    def finalize_options(self):
+        if self.bdist_dir is None:
+            bdist_base = self.get_finalized_command('bdist').bdist_base
+            self.bdist_dir = os.path.join(bdist_base, 'dumb')
+
+        if self.format is None:
+            try:
+                self.format = self.default_format[os.name]
+            except KeyError:
+                raise DistutilsPlatformError(
+                    "don't know how to create dumb built distributions "
+                    f"on platform {os.name}"
+                )
+
+        self.set_undefined_options(
+            'bdist',
+            ('dist_dir', 'dist_dir'),
+            ('plat_name', 'plat_name'),
+            ('skip_build', 'skip_build'),
+        )
+
+    def run(self):
+        if not self.skip_build:
+            self.run_command('build')
+
+        install = self.reinitialize_command('install', reinit_subcommands=True)
+        install.root = self.bdist_dir
+        install.skip_build = self.skip_build
+        install.warn_dir = False
+
+        log.info("installing to %s", self.bdist_dir)
+        self.run_command('install')
+
+        # And make an archive relative to the root of the
+        # pseudo-installation tree.
+        archive_basename = f"{self.distribution.get_fullname()}.{self.plat_name}"
+
+        pseudoinstall_root = os.path.join(self.dist_dir, archive_basename)
+        if not self.relative:
+            archive_root = self.bdist_dir
+        else:
+            if self.distribution.has_ext_modules() and (
+                install.install_base != install.install_platbase
+            ):
+                raise DistutilsPlatformError(
+                    "can't make a dumb built distribution where "
+                    f"base and platbase are different ({install.install_base!r}, {install.install_platbase!r})"
+                )
+            else:
+                archive_root = os.path.join(
+                    self.bdist_dir, ensure_relative(install.install_base)
+                )
+
+        # Make the archive
+        filename = self.make_archive(
+            pseudoinstall_root,
+            self.format,
+            root_dir=archive_root,
+            owner=self.owner,
+            group=self.group,
+        )
+        if self.distribution.has_ext_modules():
+            pyversion = get_python_version()
+        else:
+            pyversion = 'any'
+        self.distribution.dist_files.append(('bdist_dumb', pyversion, filename))
+
+        if not self.keep_temp:
+            remove_tree(self.bdist_dir, dry_run=self.dry_run)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist_rpm.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist_rpm.py
new file mode 100644
index 00000000..357b4e86
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/bdist_rpm.py
@@ -0,0 +1,598 @@
+"""distutils.command.bdist_rpm
+
+Implements the Distutils 'bdist_rpm' command (create RPM source and binary
+distributions)."""
+
+import os
+import subprocess
+import sys
+from distutils._log import log
+from typing import ClassVar
+
+from ..core import Command
+from ..debug import DEBUG
+from ..errors import (
+    DistutilsExecError,
+    DistutilsFileError,
+    DistutilsOptionError,
+    DistutilsPlatformError,
+)
+from ..file_util import write_file
+from ..sysconfig import get_python_version
+
+
+class bdist_rpm(Command):
+    description = "create an RPM distribution"
+
+    user_options = [
+        ('bdist-base=', None, "base directory for creating built distributions"),
+        (
+            'rpm-base=',
+            None,
+            "base directory for creating RPMs (defaults to \"rpm\" under "
+            "--bdist-base; must be specified for RPM 2)",
+        ),
+        (
+            'dist-dir=',
+            'd',
+            "directory to put final RPM files in (and .spec files if --spec-only)",
+        ),
+        (
+            'python=',
+            None,
+            "path to Python interpreter to hard-code in the .spec file "
+            "[default: \"python\"]",
+        ),
+        (
+            'fix-python',
+            None,
+            "hard-code the exact path to the current Python interpreter in "
+            "the .spec file",
+        ),
+        ('spec-only', None, "only regenerate spec file"),
+        ('source-only', None, "only generate source RPM"),
+        ('binary-only', None, "only generate binary RPM"),
+        ('use-bzip2', None, "use bzip2 instead of gzip to create source distribution"),
+        # More meta-data: too RPM-specific to put in the setup script,
+        # but needs to go in the .spec file -- so we make these options
+        # to "bdist_rpm".  The idea is that packagers would put this
+        # info in setup.cfg, although they are of course free to
+        # supply it on the command line.
+        (
+            'distribution-name=',
+            None,
+            "name of the (Linux) distribution to which this "
+            "RPM applies (*not* the name of the module distribution!)",
+        ),
+        ('group=', None, "package classification [default: \"Development/Libraries\"]"),
+        ('release=', None, "RPM release number"),
+        ('serial=', None, "RPM serial number"),
+        (
+            'vendor=',
+            None,
+            "RPM \"vendor\" (eg. \"Joe Blow <joe@example.com>\") "
+            "[default: maintainer or author from setup script]",
+        ),
+        (
+            'packager=',
+            None,
+            "RPM packager (eg. \"Jane Doe <jane@example.net>\") [default: vendor]",
+        ),
+        ('doc-files=', None, "list of documentation files (space or comma-separated)"),
+        ('changelog=', None, "RPM changelog"),
+        ('icon=', None, "name of icon file"),
+        ('provides=', None, "capabilities provided by this package"),
+        ('requires=', None, "capabilities required by this package"),
+        ('conflicts=', None, "capabilities which conflict with this package"),
+        ('build-requires=', None, "capabilities required to build this package"),
+        ('obsoletes=', None, "capabilities made obsolete by this package"),
+        ('no-autoreq', None, "do not automatically calculate dependencies"),
+        # Actions to take when building RPM
+        ('keep-temp', 'k', "don't clean up RPM build directory"),
+        ('no-keep-temp', None, "clean up RPM build directory [default]"),
+        (
+            'use-rpm-opt-flags',
+            None,
+            "compile with RPM_OPT_FLAGS when building from source RPM",
+        ),
+        ('no-rpm-opt-flags', None, "do not pass any RPM CFLAGS to compiler"),
+        ('rpm3-mode', None, "RPM 3 compatibility mode (default)"),
+        ('rpm2-mode', None, "RPM 2 compatibility mode"),
+        # Add the hooks necessary for specifying custom scripts
+        ('prep-script=', None, "Specify a script for the PREP phase of RPM building"),
+        ('build-script=', None, "Specify a script for the BUILD phase of RPM building"),
+        (
+            'pre-install=',
+            None,
+            "Specify a script for the pre-INSTALL phase of RPM building",
+        ),
+        (
+            'install-script=',
+            None,
+            "Specify a script for the INSTALL phase of RPM building",
+        ),
+        (
+            'post-install=',
+            None,
+            "Specify a script for the post-INSTALL phase of RPM building",
+        ),
+        (
+            'pre-uninstall=',
+            None,
+            "Specify a script for the pre-UNINSTALL phase of RPM building",
+        ),
+        (
+            'post-uninstall=',
+            None,
+            "Specify a script for the post-UNINSTALL phase of RPM building",
+        ),
+        ('clean-script=', None, "Specify a script for the CLEAN phase of RPM building"),
+        (
+            'verify-script=',
+            None,
+            "Specify a script for the VERIFY phase of the RPM build",
+        ),
+        # Allow a packager to explicitly force an architecture
+        ('force-arch=', None, "Force an architecture onto the RPM build process"),
+        ('quiet', 'q', "Run the INSTALL phase of RPM building in quiet mode"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = [
+        'keep-temp',
+        'use-rpm-opt-flags',
+        'rpm3-mode',
+        'no-autoreq',
+        'quiet',
+    ]
+
+    negative_opt: ClassVar[dict[str, str]] = {
+        'no-keep-temp': 'keep-temp',
+        'no-rpm-opt-flags': 'use-rpm-opt-flags',
+        'rpm2-mode': 'rpm3-mode',
+    }
+
+    def initialize_options(self):
+        self.bdist_base = None
+        self.rpm_base = None
+        self.dist_dir = None
+        self.python = None
+        self.fix_python = None
+        self.spec_only = None
+        self.binary_only = None
+        self.source_only = None
+        self.use_bzip2 = None
+
+        self.distribution_name = None
+        self.group = None
+        self.release = None
+        self.serial = None
+        self.vendor = None
+        self.packager = None
+        self.doc_files = None
+        self.changelog = None
+        self.icon = None
+
+        self.prep_script = None
+        self.build_script = None
+        self.install_script = None
+        self.clean_script = None
+        self.verify_script = None
+        self.pre_install = None
+        self.post_install = None
+        self.pre_uninstall = None
+        self.post_uninstall = None
+        self.prep = None
+        self.provides = None
+        self.requires = None
+        self.conflicts = None
+        self.build_requires = None
+        self.obsoletes = None
+
+        self.keep_temp = False
+        self.use_rpm_opt_flags = True
+        self.rpm3_mode = True
+        self.no_autoreq = False
+
+        self.force_arch = None
+        self.quiet = False
+
+    def finalize_options(self) -> None:
+        self.set_undefined_options('bdist', ('bdist_base', 'bdist_base'))
+        if self.rpm_base is None:
+            if not self.rpm3_mode:
+                raise DistutilsOptionError("you must specify --rpm-base in RPM 2 mode")
+            self.rpm_base = os.path.join(self.bdist_base, "rpm")
+
+        if self.python is None:
+            if self.fix_python:
+                self.python = sys.executable
+            else:
+                self.python = "python3"
+        elif self.fix_python:
+            raise DistutilsOptionError(
+                "--python and --fix-python are mutually exclusive options"
+            )
+
+        if os.name != 'posix':
+            raise DistutilsPlatformError(
+                f"don't know how to create RPM distributions on platform {os.name}"
+            )
+        if self.binary_only and self.source_only:
+            raise DistutilsOptionError(
+                "cannot supply both '--source-only' and '--binary-only'"
+            )
+
+        # don't pass CFLAGS to pure python distributions
+        if not self.distribution.has_ext_modules():
+            self.use_rpm_opt_flags = False
+
+        self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
+        self.finalize_package_data()
+
+    def finalize_package_data(self) -> None:
+        self.ensure_string('group', "Development/Libraries")
+        self.ensure_string(
+            'vendor',
+            f"{self.distribution.get_contact()} <{self.distribution.get_contact_email()}>",
+        )
+        self.ensure_string('packager')
+        self.ensure_string_list('doc_files')
+        if isinstance(self.doc_files, list):
+            for readme in ('README', 'README.txt'):
+                if os.path.exists(readme) and readme not in self.doc_files:
+                    self.doc_files.append(readme)
+
+        self.ensure_string('release', "1")
+        self.ensure_string('serial')  # should it be an int?
+
+        self.ensure_string('distribution_name')
+
+        self.ensure_string('changelog')
+        # Format changelog correctly
+        self.changelog = self._format_changelog(self.changelog)
+
+        self.ensure_filename('icon')
+
+        self.ensure_filename('prep_script')
+        self.ensure_filename('build_script')
+        self.ensure_filename('install_script')
+        self.ensure_filename('clean_script')
+        self.ensure_filename('verify_script')
+        self.ensure_filename('pre_install')
+        self.ensure_filename('post_install')
+        self.ensure_filename('pre_uninstall')
+        self.ensure_filename('post_uninstall')
+
+        # XXX don't forget we punted on summaries and descriptions -- they
+        # should be handled here eventually!
+
+        # Now *this* is some meta-data that belongs in the setup script...
+        self.ensure_string_list('provides')
+        self.ensure_string_list('requires')
+        self.ensure_string_list('conflicts')
+        self.ensure_string_list('build_requires')
+        self.ensure_string_list('obsoletes')
+
+        self.ensure_string('force_arch')
+
+    def run(self) -> None:  # noqa: C901
+        if DEBUG:
+            print("before _get_package_data():")
+            print("vendor =", self.vendor)
+            print("packager =", self.packager)
+            print("doc_files =", self.doc_files)
+            print("changelog =", self.changelog)
+
+        # make directories
+        if self.spec_only:
+            spec_dir = self.dist_dir
+            self.mkpath(spec_dir)
+        else:
+            rpm_dir = {}
+            for d in ('SOURCES', 'SPECS', 'BUILD', 'RPMS', 'SRPMS'):
+                rpm_dir[d] = os.path.join(self.rpm_base, d)
+                self.mkpath(rpm_dir[d])
+            spec_dir = rpm_dir['SPECS']
+
+        # Spec file goes into 'dist_dir' if '--spec-only specified',
+        # build/rpm.<plat> otherwise.
+        spec_path = os.path.join(spec_dir, f"{self.distribution.get_name()}.spec")
+        self.execute(
+            write_file, (spec_path, self._make_spec_file()), f"writing '{spec_path}'"
+        )
+
+        if self.spec_only:  # stop if requested
+            return
+
+        # Make a source distribution and copy to SOURCES directory with
+        # optional icon.
+        saved_dist_files = self.distribution.dist_files[:]
+        sdist = self.reinitialize_command('sdist')
+        if self.use_bzip2:
+            sdist.formats = ['bztar']
+        else:
+            sdist.formats = ['gztar']
+        self.run_command('sdist')
+        self.distribution.dist_files = saved_dist_files
+
+        source = sdist.get_archive_files()[0]
+        source_dir = rpm_dir['SOURCES']
+        self.copy_file(source, source_dir)
+
+        if self.icon:
+            if os.path.exists(self.icon):
+                self.copy_file(self.icon, source_dir)
+            else:
+                raise DistutilsFileError(f"icon file '{self.icon}' does not exist")
+
+        # build package
+        log.info("building RPMs")
+        rpm_cmd = ['rpmbuild']
+
+        if self.source_only:  # what kind of RPMs?
+            rpm_cmd.append('-bs')
+        elif self.binary_only:
+            rpm_cmd.append('-bb')
+        else:
+            rpm_cmd.append('-ba')
+        rpm_cmd.extend(['--define', f'__python {self.python}'])
+        if self.rpm3_mode:
+            rpm_cmd.extend(['--define', f'_topdir {os.path.abspath(self.rpm_base)}'])
+        if not self.keep_temp:
+            rpm_cmd.append('--clean')
+
+        if self.quiet:
+            rpm_cmd.append('--quiet')
+
+        rpm_cmd.append(spec_path)
+        # Determine the binary rpm names that should be built out of this spec
+        # file
+        # Note that some of these may not be really built (if the file
+        # list is empty)
+        nvr_string = "%{name}-%{version}-%{release}"
+        src_rpm = nvr_string + ".src.rpm"
+        non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm"
+        q_cmd = rf"rpm -q --qf '{src_rpm} {non_src_rpm}\n' --specfile '{spec_path}'"
+
+        out = os.popen(q_cmd)
+        try:
+            binary_rpms = []
+            source_rpm = None
+            while True:
+                line = out.readline()
+                if not line:
+                    break
+                ell = line.strip().split()
+                assert len(ell) == 2
+                binary_rpms.append(ell[1])
+                # The source rpm is named after the first entry in the spec file
+                if source_rpm is None:
+                    source_rpm = ell[0]
+
+            status = out.close()
+            if status:
+                raise DistutilsExecError(f"Failed to execute: {q_cmd!r}")
+
+        finally:
+            out.close()
+
+        self.spawn(rpm_cmd)
+
+        if not self.dry_run:
+            if self.distribution.has_ext_modules():
+                pyversion = get_python_version()
+            else:
+                pyversion = 'any'
+
+            if not self.binary_only:
+                srpm = os.path.join(rpm_dir['SRPMS'], source_rpm)
+                assert os.path.exists(srpm)
+                self.move_file(srpm, self.dist_dir)
+                filename = os.path.join(self.dist_dir, source_rpm)
+                self.distribution.dist_files.append(('bdist_rpm', pyversion, filename))
+
+            if not self.source_only:
+                for rpm in binary_rpms:
+                    rpm = os.path.join(rpm_dir['RPMS'], rpm)
+                    if os.path.exists(rpm):
+                        self.move_file(rpm, self.dist_dir)
+                        filename = os.path.join(self.dist_dir, os.path.basename(rpm))
+                        self.distribution.dist_files.append((
+                            'bdist_rpm',
+                            pyversion,
+                            filename,
+                        ))
+
+    def _dist_path(self, path):
+        return os.path.join(self.dist_dir, os.path.basename(path))
+
+    def _make_spec_file(self):  # noqa: C901
+        """Generate the text of an RPM spec file and return it as a
+        list of strings (one per line).
+        """
+        # definitions and headers
+        spec_file = [
+            '%define name ' + self.distribution.get_name(),
+            '%define version ' + self.distribution.get_version().replace('-', '_'),
+            '%define unmangled_version ' + self.distribution.get_version(),
+            '%define release ' + self.release.replace('-', '_'),
+            '',
+            'Summary: ' + (self.distribution.get_description() or "UNKNOWN"),
+        ]
+
+        # Workaround for #14443 which affects some RPM based systems such as
+        # RHEL6 (and probably derivatives)
+        vendor_hook = subprocess.getoutput('rpm --eval %{__os_install_post}')
+        # Generate a potential replacement value for __os_install_post (whilst
+        # normalizing the whitespace to simplify the test for whether the
+        # invocation of brp-python-bytecompile passes in __python):
+        vendor_hook = '\n'.join([
+            f'  {line.strip()} \\' for line in vendor_hook.splitlines()
+        ])
+        problem = "brp-python-bytecompile \\\n"
+        fixed = "brp-python-bytecompile %{__python} \\\n"
+        fixed_hook = vendor_hook.replace(problem, fixed)
+        if fixed_hook != vendor_hook:
+            spec_file.append('# Workaround for https://bugs.python.org/issue14443')
+            spec_file.append('%define __os_install_post ' + fixed_hook + '\n')
+
+        # put locale summaries into spec file
+        # XXX not supported for now (hard to put a dictionary
+        # in a config file -- arg!)
+        # for locale in self.summaries.keys():
+        #    spec_file.append('Summary(%s): %s' % (locale,
+        #                                          self.summaries[locale]))
+
+        spec_file.extend([
+            'Name: %{name}',
+            'Version: %{version}',
+            'Release: %{release}',
+        ])
+
+        # XXX yuck! this filename is available from the "sdist" command,
+        # but only after it has run: and we create the spec file before
+        # running "sdist", in case of --spec-only.
+        if self.use_bzip2:
+            spec_file.append('Source0: %{name}-%{unmangled_version}.tar.bz2')
+        else:
+            spec_file.append('Source0: %{name}-%{unmangled_version}.tar.gz')
+
+        spec_file.extend([
+            'License: ' + (self.distribution.get_license() or "UNKNOWN"),
+            'Group: ' + self.group,
+            'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot',
+            'Prefix: %{_prefix}',
+        ])
+
+        if not self.force_arch:
+            # noarch if no extension modules
+            if not self.distribution.has_ext_modules():
+                spec_file.append('BuildArch: noarch')
+        else:
+            spec_file.append(f'BuildArch: {self.force_arch}')
+
+        for field in (
+            'Vendor',
+            'Packager',
+            'Provides',
+            'Requires',
+            'Conflicts',
+            'Obsoletes',
+        ):
+            val = getattr(self, field.lower())
+            if isinstance(val, list):
+                spec_file.append('{}: {}'.format(field, ' '.join(val)))
+            elif val is not None:
+                spec_file.append(f'{field}: {val}')
+
+        if self.distribution.get_url():
+            spec_file.append('Url: ' + self.distribution.get_url())
+
+        if self.distribution_name:
+            spec_file.append('Distribution: ' + self.distribution_name)
+
+        if self.build_requires:
+            spec_file.append('BuildRequires: ' + ' '.join(self.build_requires))
+
+        if self.icon:
+            spec_file.append('Icon: ' + os.path.basename(self.icon))
+
+        if self.no_autoreq:
+            spec_file.append('AutoReq: 0')
+
+        spec_file.extend([
+            '',
+            '%description',
+            self.distribution.get_long_description() or "",
+        ])
+
+        # put locale descriptions into spec file
+        # XXX again, suppressed because config file syntax doesn't
+        # easily support this ;-(
+        # for locale in self.descriptions.keys():
+        #    spec_file.extend([
+        #        '',
+        #        '%description -l ' + locale,
+        #        self.descriptions[locale],
+        #        ])
+
+        # rpm scripts
+        # figure out default build script
+        def_setup_call = f"{self.python} {os.path.basename(sys.argv[0])}"
+        def_build = f"{def_setup_call} build"
+        if self.use_rpm_opt_flags:
+            def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build
+
+        # insert contents of files
+
+        # XXX this is kind of misleading: user-supplied options are files
+        # that we open and interpolate into the spec file, but the defaults
+        # are just text that we drop in as-is.  Hmmm.
+
+        install_cmd = f'{def_setup_call} install -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES'
+
+        script_options = [
+            ('prep', 'prep_script', "%setup -n %{name}-%{unmangled_version}"),
+            ('build', 'build_script', def_build),
+            ('install', 'install_script', install_cmd),
+            ('clean', 'clean_script', "rm -rf $RPM_BUILD_ROOT"),
+            ('verifyscript', 'verify_script', None),
+            ('pre', 'pre_install', None),
+            ('post', 'post_install', None),
+            ('preun', 'pre_uninstall', None),
+            ('postun', 'post_uninstall', None),
+        ]
+
+        for rpm_opt, attr, default in script_options:
+            # Insert contents of file referred to, if no file is referred to
+            # use 'default' as contents of script
+            val = getattr(self, attr)
+            if val or default:
+                spec_file.extend([
+                    '',
+                    '%' + rpm_opt,
+                ])
+                if val:
+                    with open(val) as f:
+                        spec_file.extend(f.read().split('\n'))
+                else:
+                    spec_file.append(default)
+
+        # files section
+        spec_file.extend([
+            '',
+            '%files -f INSTALLED_FILES',
+            '%defattr(-,root,root)',
+        ])
+
+        if self.doc_files:
+            spec_file.append('%doc ' + ' '.join(self.doc_files))
+
+        if self.changelog:
+            spec_file.extend([
+                '',
+                '%changelog',
+            ])
+            spec_file.extend(self.changelog)
+
+        return spec_file
+
+    def _format_changelog(self, changelog):
+        """Format the changelog correctly and convert it to a list of strings"""
+        if not changelog:
+            return changelog
+        new_changelog = []
+        for line in changelog.strip().split('\n'):
+            line = line.strip()
+            if line[0] == '*':
+                new_changelog.extend(['', line])
+            elif line[0] == '-':
+                new_changelog.append(line)
+            else:
+                new_changelog.append('  ' + line)
+
+        # strip trailing newline inserted by first changelog entry
+        if not new_changelog[0]:
+            del new_changelog[0]
+
+        return new_changelog
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build.py
new file mode 100644
index 00000000..6a8303a9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build.py
@@ -0,0 +1,156 @@
+"""distutils.command.build
+
+Implements the Distutils 'build' command."""
+
+from __future__ import annotations
+
+import os
+import sys
+import sysconfig
+from collections.abc import Callable
+from typing import ClassVar
+
+from ..ccompiler import show_compilers
+from ..core import Command
+from ..errors import DistutilsOptionError
+from ..util import get_platform
+
+
+class build(Command):
+    description = "build everything needed to install"
+
+    user_options = [
+        ('build-base=', 'b', "base directory for build library"),
+        ('build-purelib=', None, "build directory for platform-neutral distributions"),
+        ('build-platlib=', None, "build directory for platform-specific distributions"),
+        (
+            'build-lib=',
+            None,
+            "build directory for all distribution (defaults to either build-purelib or build-platlib",
+        ),
+        ('build-scripts=', None, "build directory for scripts"),
+        ('build-temp=', 't', "temporary build directory"),
+        (
+            'plat-name=',
+            'p',
+            f"platform name to build for, if supported [default: {get_platform()}]",
+        ),
+        ('compiler=', 'c', "specify the compiler type"),
+        ('parallel=', 'j', "number of parallel build jobs"),
+        ('debug', 'g', "compile extensions and libraries with debugging information"),
+        ('force', 'f', "forcibly build everything (ignore file timestamps)"),
+        ('executable=', 'e', "specify final destination interpreter path (build.py)"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['debug', 'force']
+
+    help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
+        ('help-compiler', None, "list available compilers", show_compilers),
+    ]
+
+    def initialize_options(self):
+        self.build_base = 'build'
+        # these are decided only after 'build_base' has its final value
+        # (unless overridden by the user or client)
+        self.build_purelib = None
+        self.build_platlib = None
+        self.build_lib = None
+        self.build_temp = None
+        self.build_scripts = None
+        self.compiler = None
+        self.plat_name = None
+        self.debug = None
+        self.force = False
+        self.executable = None
+        self.parallel = None
+
+    def finalize_options(self) -> None:  # noqa: C901
+        if self.plat_name is None:
+            self.plat_name = get_platform()
+        else:
+            # plat-name only supported for windows (other platforms are
+            # supported via ./configure flags, if at all).  Avoid misleading
+            # other platforms.
+            if os.name != 'nt':
+                raise DistutilsOptionError(
+                    "--plat-name only supported on Windows (try "
+                    "using './configure --help' on your platform)"
+                )
+
+        plat_specifier = f".{self.plat_name}-{sys.implementation.cache_tag}"
+
+        # Python 3.13+ with --disable-gil shouldn't share build directories
+        if sysconfig.get_config_var('Py_GIL_DISABLED'):
+            plat_specifier += 't'
+
+        # Make it so Python 2.x and Python 2.x with --with-pydebug don't
+        # share the same build directories. Doing so confuses the build
+        # process for C modules
+        if hasattr(sys, 'gettotalrefcount'):
+            plat_specifier += '-pydebug'
+
+        # 'build_purelib' and 'build_platlib' just default to 'lib' and
+        # 'lib.<plat>' under the base build directory.  We only use one of
+        # them for a given distribution, though --
+        if self.build_purelib is None:
+            self.build_purelib = os.path.join(self.build_base, 'lib')
+        if self.build_platlib is None:
+            self.build_platlib = os.path.join(self.build_base, 'lib' + plat_specifier)
+
+        # 'build_lib' is the actual directory that we will use for this
+        # particular module distribution -- if user didn't supply it, pick
+        # one of 'build_purelib' or 'build_platlib'.
+        if self.build_lib is None:
+            if self.distribution.has_ext_modules():
+                self.build_lib = self.build_platlib
+            else:
+                self.build_lib = self.build_purelib
+
+        # 'build_temp' -- temporary directory for compiler turds,
+        # "build/temp.<plat>"
+        if self.build_temp is None:
+            self.build_temp = os.path.join(self.build_base, 'temp' + plat_specifier)
+        if self.build_scripts is None:
+            self.build_scripts = os.path.join(
+                self.build_base,
+                f'scripts-{sys.version_info.major}.{sys.version_info.minor}',
+            )
+
+        if self.executable is None and sys.executable:
+            self.executable = os.path.normpath(sys.executable)
+
+        if isinstance(self.parallel, str):
+            try:
+                self.parallel = int(self.parallel)
+            except ValueError:
+                raise DistutilsOptionError("parallel should be an integer")
+
+    def run(self) -> None:
+        # Run all relevant sub-commands.  This will be some subset of:
+        #  - build_py      - pure Python modules
+        #  - build_clib    - standalone C libraries
+        #  - build_ext     - Python extensions
+        #  - build_scripts - (Python) scripts
+        for cmd_name in self.get_sub_commands():
+            self.run_command(cmd_name)
+
+    # -- Predicates for the sub-command list ---------------------------
+
+    def has_pure_modules(self):
+        return self.distribution.has_pure_modules()
+
+    def has_c_libraries(self):
+        return self.distribution.has_c_libraries()
+
+    def has_ext_modules(self):
+        return self.distribution.has_ext_modules()
+
+    def has_scripts(self):
+        return self.distribution.has_scripts()
+
+    sub_commands = [
+        ('build_py', has_pure_modules),
+        ('build_clib', has_c_libraries),
+        ('build_ext', has_ext_modules),
+        ('build_scripts', has_scripts),
+    ]
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_clib.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_clib.py
new file mode 100644
index 00000000..8b65b3d8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_clib.py
@@ -0,0 +1,201 @@
+"""distutils.command.build_clib
+
+Implements the Distutils 'build_clib' command, to build a C/C++ library
+that is included in the module distribution and needed by an extension
+module."""
+
+# XXX this module has *lots* of code ripped-off quite transparently from
+# build_ext.py -- not surprisingly really, as the work required to build
+# a static library from a collection of C source files is not really all
+# that different from what's required to build a shared object file from
+# a collection of C source files.  Nevertheless, I haven't done the
+# necessary refactoring to account for the overlap in code between the
+# two modules, mainly because a number of subtle details changed in the
+# cut 'n paste.  Sigh.
+from __future__ import annotations
+
+import os
+from collections.abc import Callable
+from distutils._log import log
+from typing import ClassVar
+
+from ..ccompiler import new_compiler, show_compilers
+from ..core import Command
+from ..errors import DistutilsSetupError
+from ..sysconfig import customize_compiler
+
+
+class build_clib(Command):
+    description = "build C/C++ libraries used by Python extensions"
+
+    user_options: ClassVar[list[tuple[str, str, str]]] = [
+        ('build-clib=', 'b', "directory to build C/C++ libraries to"),
+        ('build-temp=', 't', "directory to put temporary build by-products"),
+        ('debug', 'g', "compile with debugging information"),
+        ('force', 'f', "forcibly build everything (ignore file timestamps)"),
+        ('compiler=', 'c', "specify the compiler type"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['debug', 'force']
+
+    help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
+        ('help-compiler', None, "list available compilers", show_compilers),
+    ]
+
+    def initialize_options(self):
+        self.build_clib = None
+        self.build_temp = None
+
+        # List of libraries to build
+        self.libraries = None
+
+        # Compilation options for all libraries
+        self.include_dirs = None
+        self.define = None
+        self.undef = None
+        self.debug = None
+        self.force = False
+        self.compiler = None
+
+    def finalize_options(self) -> None:
+        # This might be confusing: both build-clib and build-temp default
+        # to build-temp as defined by the "build" command.  This is because
+        # I think that C libraries are really just temporary build
+        # by-products, at least from the point of view of building Python
+        # extensions -- but I want to keep my options open.
+        self.set_undefined_options(
+            'build',
+            ('build_temp', 'build_clib'),
+            ('build_temp', 'build_temp'),
+            ('compiler', 'compiler'),
+            ('debug', 'debug'),
+            ('force', 'force'),
+        )
+
+        self.libraries = self.distribution.libraries
+        if self.libraries:
+            self.check_library_list(self.libraries)
+
+        if self.include_dirs is None:
+            self.include_dirs = self.distribution.include_dirs or []
+        if isinstance(self.include_dirs, str):
+            self.include_dirs = self.include_dirs.split(os.pathsep)
+
+        # XXX same as for build_ext -- what about 'self.define' and
+        # 'self.undef' ?
+
+    def run(self) -> None:
+        if not self.libraries:
+            return
+
+        self.compiler = new_compiler(
+            compiler=self.compiler, dry_run=self.dry_run, force=self.force
+        )
+        customize_compiler(self.compiler)
+
+        if self.include_dirs is not None:
+            self.compiler.set_include_dirs(self.include_dirs)
+        if self.define is not None:
+            # 'define' option is a list of (name,value) tuples
+            for name, value in self.define:
+                self.compiler.define_macro(name, value)
+        if self.undef is not None:
+            for macro in self.undef:
+                self.compiler.undefine_macro(macro)
+
+        self.build_libraries(self.libraries)
+
+    def check_library_list(self, libraries) -> None:
+        """Ensure that the list of libraries is valid.
+
+        `library` is presumably provided as a command option 'libraries'.
+        This method checks that it is a list of 2-tuples, where the tuples
+        are (library_name, build_info_dict).
+
+        Raise DistutilsSetupError if the structure is invalid anywhere;
+        just returns otherwise.
+        """
+        if not isinstance(libraries, list):
+            raise DistutilsSetupError("'libraries' option must be a list of tuples")
+
+        for lib in libraries:
+            if not isinstance(lib, tuple) and len(lib) != 2:
+                raise DistutilsSetupError("each element of 'libraries' must a 2-tuple")
+
+            name, build_info = lib
+
+            if not isinstance(name, str):
+                raise DistutilsSetupError(
+                    "first element of each tuple in 'libraries' "
+                    "must be a string (the library name)"
+                )
+
+            if '/' in name or (os.sep != '/' and os.sep in name):
+                raise DistutilsSetupError(
+                    f"bad library name '{lib[0]}': may not contain directory separators"
+                )
+
+            if not isinstance(build_info, dict):
+                raise DistutilsSetupError(
+                    "second element of each tuple in 'libraries' "
+                    "must be a dictionary (build info)"
+                )
+
+    def get_library_names(self):
+        # Assume the library list is valid -- 'check_library_list()' is
+        # called from 'finalize_options()', so it should be!
+        if not self.libraries:
+            return None
+
+        lib_names = []
+        for lib_name, _build_info in self.libraries:
+            lib_names.append(lib_name)
+        return lib_names
+
+    def get_source_files(self):
+        self.check_library_list(self.libraries)
+        filenames = []
+        for lib_name, build_info in self.libraries:
+            sources = build_info.get('sources')
+            if sources is None or not isinstance(sources, (list, tuple)):
+                raise DistutilsSetupError(
+                    f"in 'libraries' option (library '{lib_name}'), "
+                    "'sources' must be present and must be "
+                    "a list of source filenames"
+                )
+
+            filenames.extend(sources)
+        return filenames
+
+    def build_libraries(self, libraries) -> None:
+        for lib_name, build_info in libraries:
+            sources = build_info.get('sources')
+            if sources is None or not isinstance(sources, (list, tuple)):
+                raise DistutilsSetupError(
+                    f"in 'libraries' option (library '{lib_name}'), "
+                    "'sources' must be present and must be "
+                    "a list of source filenames"
+                )
+            sources = list(sources)
+
+            log.info("building '%s' library", lib_name)
+
+            # First, compile the source code to object files in the library
+            # directory.  (This should probably change to putting object
+            # files in a temporary build directory.)
+            macros = build_info.get('macros')
+            include_dirs = build_info.get('include_dirs')
+            objects = self.compiler.compile(
+                sources,
+                output_dir=self.build_temp,
+                macros=macros,
+                include_dirs=include_dirs,
+                debug=self.debug,
+            )
+
+            # Now "link" the object files together into a static library.
+            # (On Unix at least, this isn't really linking -- it just
+            # builds an archive.  Whatever.)
+            self.compiler.create_static_lib(
+                objects, lib_name, output_dir=self.build_clib, debug=self.debug
+            )
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_ext.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_ext.py
new file mode 100644
index 00000000..ec45b440
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_ext.py
@@ -0,0 +1,812 @@
+"""distutils.command.build_ext
+
+Implements the Distutils 'build_ext' command, for building extension
+modules (currently limited to C extensions, should accommodate C++
+extensions ASAP)."""
+
+from __future__ import annotations
+
+import contextlib
+import os
+import re
+import sys
+from collections.abc import Callable
+from distutils._log import log
+from site import USER_BASE
+from typing import ClassVar
+
+from .._modified import newer_group
+from ..ccompiler import new_compiler, show_compilers
+from ..core import Command
+from ..errors import (
+    CCompilerError,
+    CompileError,
+    DistutilsError,
+    DistutilsOptionError,
+    DistutilsPlatformError,
+    DistutilsSetupError,
+)
+from ..extension import Extension
+from ..sysconfig import customize_compiler, get_config_h_filename, get_python_version
+from ..util import get_platform, is_freethreaded, is_mingw
+
+# An extension name is just a dot-separated list of Python NAMEs (ie.
+# the same as a fully-qualified module name).
+extension_name_re = re.compile(r'^[a-zA-Z_][a-zA-Z_0-9]*(\.[a-zA-Z_][a-zA-Z_0-9]*)*$')
+
+
+class build_ext(Command):
+    description = "build C/C++ extensions (compile/link to build directory)"
+
+    # XXX thoughts on how to deal with complex command-line options like
+    # these, i.e. how to make it so fancy_getopt can suck them off the
+    # command line and make it look like setup.py defined the appropriate
+    # lists of tuples of what-have-you.
+    #   - each command needs a callback to process its command-line options
+    #   - Command.__init__() needs access to its share of the whole
+    #     command line (must ultimately come from
+    #     Distribution.parse_command_line())
+    #   - it then calls the current command class' option-parsing
+    #     callback to deal with weird options like -D, which have to
+    #     parse the option text and churn out some custom data
+    #     structure
+    #   - that data structure (in this case, a list of 2-tuples)
+    #     will then be present in the command object by the time
+    #     we get to finalize_options() (i.e. the constructor
+    #     takes care of both command-line and client options
+    #     in between initialize_options() and finalize_options())
+
+    sep_by = f" (separated by '{os.pathsep}')"
+    user_options = [
+        ('build-lib=', 'b', "directory for compiled extension modules"),
+        ('build-temp=', 't', "directory for temporary files (build by-products)"),
+        (
+            'plat-name=',
+            'p',
+            "platform name to cross-compile for, if supported "
+            f"[default: {get_platform()}]",
+        ),
+        (
+            'inplace',
+            'i',
+            "ignore build-lib and put compiled extensions into the source "
+            "directory alongside your pure Python modules",
+        ),
+        (
+            'include-dirs=',
+            'I',
+            "list of directories to search for header files" + sep_by,
+        ),
+        ('define=', 'D', "C preprocessor macros to define"),
+        ('undef=', 'U', "C preprocessor macros to undefine"),
+        ('libraries=', 'l', "external C libraries to link with"),
+        (
+            'library-dirs=',
+            'L',
+            "directories to search for external C libraries" + sep_by,
+        ),
+        ('rpath=', 'R', "directories to search for shared C libraries at runtime"),
+        ('link-objects=', 'O', "extra explicit link objects to include in the link"),
+        ('debug', 'g', "compile/link with debugging information"),
+        ('force', 'f', "forcibly build everything (ignore file timestamps)"),
+        ('compiler=', 'c', "specify the compiler type"),
+        ('parallel=', 'j', "number of parallel build jobs"),
+        ('swig-cpp', None, "make SWIG create C++ files (default is C)"),
+        ('swig-opts=', None, "list of SWIG command line options"),
+        ('swig=', None, "path to the SWIG executable"),
+        ('user', None, "add user include, library and rpath"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = [
+        'inplace',
+        'debug',
+        'force',
+        'swig-cpp',
+        'user',
+    ]
+
+    help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
+        ('help-compiler', None, "list available compilers", show_compilers),
+    ]
+
+    def initialize_options(self):
+        self.extensions = None
+        self.build_lib = None
+        self.plat_name = None
+        self.build_temp = None
+        self.inplace = False
+        self.package = None
+
+        self.include_dirs = None
+        self.define = None
+        self.undef = None
+        self.libraries = None
+        self.library_dirs = None
+        self.rpath = None
+        self.link_objects = None
+        self.debug = None
+        self.force = None
+        self.compiler = None
+        self.swig = None
+        self.swig_cpp = None
+        self.swig_opts = None
+        self.user = None
+        self.parallel = None
+
+    @staticmethod
+    def _python_lib_dir(sysconfig):
+        """
+        Resolve Python's library directory for building extensions
+        that rely on a shared Python library.
+
+        See python/cpython#44264 and python/cpython#48686
+        """
+        if not sysconfig.get_config_var('Py_ENABLE_SHARED'):
+            return
+
+        if sysconfig.python_build:
+            yield '.'
+            return
+
+        if sys.platform == 'zos':
+            # On z/OS, a user is not required to install Python to
+            # a predetermined path, but can use Python portably
+            installed_dir = sysconfig.get_config_var('base')
+            lib_dir = sysconfig.get_config_var('platlibdir')
+            yield os.path.join(installed_dir, lib_dir)
+        else:
+            # building third party extensions
+            yield sysconfig.get_config_var('LIBDIR')
+
+    def finalize_options(self) -> None:  # noqa: C901
+        from distutils import sysconfig
+
+        self.set_undefined_options(
+            'build',
+            ('build_lib', 'build_lib'),
+            ('build_temp', 'build_temp'),
+            ('compiler', 'compiler'),
+            ('debug', 'debug'),
+            ('force', 'force'),
+            ('parallel', 'parallel'),
+            ('plat_name', 'plat_name'),
+        )
+
+        if self.package is None:
+            self.package = self.distribution.ext_package
+
+        self.extensions = self.distribution.ext_modules
+
+        # Make sure Python's include directories (for Python.h, pyconfig.h,
+        # etc.) are in the include search path.
+        py_include = sysconfig.get_python_inc()
+        plat_py_include = sysconfig.get_python_inc(plat_specific=True)
+        if self.include_dirs is None:
+            self.include_dirs = self.distribution.include_dirs or []
+        if isinstance(self.include_dirs, str):
+            self.include_dirs = self.include_dirs.split(os.pathsep)
+
+        # If in a virtualenv, add its include directory
+        # Issue 16116
+        if sys.exec_prefix != sys.base_exec_prefix:
+            self.include_dirs.append(os.path.join(sys.exec_prefix, 'include'))
+
+        # Put the Python "system" include dir at the end, so that
+        # any local include dirs take precedence.
+        self.include_dirs.extend(py_include.split(os.path.pathsep))
+        if plat_py_include != py_include:
+            self.include_dirs.extend(plat_py_include.split(os.path.pathsep))
+
+        self.ensure_string_list('libraries')
+        self.ensure_string_list('link_objects')
+
+        # Life is easier if we're not forever checking for None, so
+        # simplify these options to empty lists if unset
+        if self.libraries is None:
+            self.libraries = []
+        if self.library_dirs is None:
+            self.library_dirs = []
+        elif isinstance(self.library_dirs, str):
+            self.library_dirs = self.library_dirs.split(os.pathsep)
+
+        if self.rpath is None:
+            self.rpath = []
+        elif isinstance(self.rpath, str):
+            self.rpath = self.rpath.split(os.pathsep)
+
+        # for extensions under windows use different directories
+        # for Release and Debug builds.
+        # also Python's library directory must be appended to library_dirs
+        if os.name == 'nt' and not is_mingw():
+            # the 'libs' directory is for binary installs - we assume that
+            # must be the *native* platform.  But we don't really support
+            # cross-compiling via a binary install anyway, so we let it go.
+            self.library_dirs.append(os.path.join(sys.exec_prefix, 'libs'))
+            if sys.base_exec_prefix != sys.prefix:  # Issue 16116
+                self.library_dirs.append(os.path.join(sys.base_exec_prefix, 'libs'))
+            if self.debug:
+                self.build_temp = os.path.join(self.build_temp, "Debug")
+            else:
+                self.build_temp = os.path.join(self.build_temp, "Release")
+
+            # Append the source distribution include and library directories,
+            # this allows distutils on windows to work in the source tree
+            self.include_dirs.append(os.path.dirname(get_config_h_filename()))
+            self.library_dirs.append(sys.base_exec_prefix)
+
+            # Use the .lib files for the correct architecture
+            if self.plat_name == 'win32':
+                suffix = 'win32'
+            else:
+                # win-amd64
+                suffix = self.plat_name[4:]
+            new_lib = os.path.join(sys.exec_prefix, 'PCbuild')
+            if suffix:
+                new_lib = os.path.join(new_lib, suffix)
+            self.library_dirs.append(new_lib)
+
+        # For extensions under Cygwin, Python's library directory must be
+        # appended to library_dirs
+        if sys.platform[:6] == 'cygwin':
+            if not sysconfig.python_build:
+                # building third party extensions
+                self.library_dirs.append(
+                    os.path.join(
+                        sys.prefix, "lib", "python" + get_python_version(), "config"
+                    )
+                )
+            else:
+                # building python standard extensions
+                self.library_dirs.append('.')
+
+        self.library_dirs.extend(self._python_lib_dir(sysconfig))
+
+        # The argument parsing will result in self.define being a string, but
+        # it has to be a list of 2-tuples.  All the preprocessor symbols
+        # specified by the 'define' option will be set to '1'.  Multiple
+        # symbols can be separated with commas.
+
+        if self.define:
+            defines = self.define.split(',')
+            self.define = [(symbol, '1') for symbol in defines]
+
+        # The option for macros to undefine is also a string from the
+        # option parsing, but has to be a list.  Multiple symbols can also
+        # be separated with commas here.
+        if self.undef:
+            self.undef = self.undef.split(',')
+
+        if self.swig_opts is None:
+            self.swig_opts = []
+        else:
+            self.swig_opts = self.swig_opts.split(' ')
+
+        # Finally add the user include and library directories if requested
+        if self.user:
+            user_include = os.path.join(USER_BASE, "include")
+            user_lib = os.path.join(USER_BASE, "lib")
+            if os.path.isdir(user_include):
+                self.include_dirs.append(user_include)
+            if os.path.isdir(user_lib):
+                self.library_dirs.append(user_lib)
+                self.rpath.append(user_lib)
+
+        if isinstance(self.parallel, str):
+            try:
+                self.parallel = int(self.parallel)
+            except ValueError:
+                raise DistutilsOptionError("parallel should be an integer")
+
+    def run(self) -> None:  # noqa: C901
+        # 'self.extensions', as supplied by setup.py, is a list of
+        # Extension instances.  See the documentation for Extension (in
+        # distutils.extension) for details.
+        #
+        # For backwards compatibility with Distutils 0.8.2 and earlier, we
+        # also allow the 'extensions' list to be a list of tuples:
+        #    (ext_name, build_info)
+        # where build_info is a dictionary containing everything that
+        # Extension instances do except the name, with a few things being
+        # differently named.  We convert these 2-tuples to Extension
+        # instances as needed.
+
+        if not self.extensions:
+            return
+
+        # If we were asked to build any C/C++ libraries, make sure that the
+        # directory where we put them is in the library search path for
+        # linking extensions.
+        if self.distribution.has_c_libraries():
+            build_clib = self.get_finalized_command('build_clib')
+            self.libraries.extend(build_clib.get_library_names() or [])
+            self.library_dirs.append(build_clib.build_clib)
+
+        # Setup the CCompiler object that we'll use to do all the
+        # compiling and linking
+        self.compiler = new_compiler(
+            compiler=self.compiler,
+            verbose=self.verbose,
+            dry_run=self.dry_run,
+            force=self.force,
+        )
+        customize_compiler(self.compiler)
+        # If we are cross-compiling, init the compiler now (if we are not
+        # cross-compiling, init would not hurt, but people may rely on
+        # late initialization of compiler even if they shouldn't...)
+        if os.name == 'nt' and self.plat_name != get_platform():
+            self.compiler.initialize(self.plat_name)
+
+        # The official Windows free threaded Python installer doesn't set
+        # Py_GIL_DISABLED because its pyconfig.h is shared with the
+        # default build, so define it here (pypa/setuptools#4662).
+        if os.name == 'nt' and is_freethreaded():
+            self.compiler.define_macro('Py_GIL_DISABLED', '1')
+
+        # And make sure that any compile/link-related options (which might
+        # come from the command-line or from the setup script) are set in
+        # that CCompiler object -- that way, they automatically apply to
+        # all compiling and linking done here.
+        if self.include_dirs is not None:
+            self.compiler.set_include_dirs(self.include_dirs)
+        if self.define is not None:
+            # 'define' option is a list of (name,value) tuples
+            for name, value in self.define:
+                self.compiler.define_macro(name, value)
+        if self.undef is not None:
+            for macro in self.undef:
+                self.compiler.undefine_macro(macro)
+        if self.libraries is not None:
+            self.compiler.set_libraries(self.libraries)
+        if self.library_dirs is not None:
+            self.compiler.set_library_dirs(self.library_dirs)
+        if self.rpath is not None:
+            self.compiler.set_runtime_library_dirs(self.rpath)
+        if self.link_objects is not None:
+            self.compiler.set_link_objects(self.link_objects)
+
+        # Now actually compile and link everything.
+        self.build_extensions()
+
+    def check_extensions_list(self, extensions) -> None:  # noqa: C901
+        """Ensure that the list of extensions (presumably provided as a
+        command option 'extensions') is valid, i.e. it is a list of
+        Extension objects.  We also support the old-style list of 2-tuples,
+        where the tuples are (ext_name, build_info), which are converted to
+        Extension instances here.
+
+        Raise DistutilsSetupError if the structure is invalid anywhere;
+        just returns otherwise.
+        """
+        if not isinstance(extensions, list):
+            raise DistutilsSetupError(
+                "'ext_modules' option must be a list of Extension instances"
+            )
+
+        for i, ext in enumerate(extensions):
+            if isinstance(ext, Extension):
+                continue  # OK! (assume type-checking done
+                # by Extension constructor)
+
+            if not isinstance(ext, tuple) or len(ext) != 2:
+                raise DistutilsSetupError(
+                    "each element of 'ext_modules' option must be an "
+                    "Extension instance or 2-tuple"
+                )
+
+            ext_name, build_info = ext
+
+            log.warning(
+                "old-style (ext_name, build_info) tuple found in "
+                "ext_modules for extension '%s' "
+                "-- please convert to Extension instance",
+                ext_name,
+            )
+
+            if not (isinstance(ext_name, str) and extension_name_re.match(ext_name)):
+                raise DistutilsSetupError(
+                    "first element of each tuple in 'ext_modules' "
+                    "must be the extension name (a string)"
+                )
+
+            if not isinstance(build_info, dict):
+                raise DistutilsSetupError(
+                    "second element of each tuple in 'ext_modules' "
+                    "must be a dictionary (build info)"
+                )
+
+            # OK, the (ext_name, build_info) dict is type-safe: convert it
+            # to an Extension instance.
+            ext = Extension(ext_name, build_info['sources'])
+
+            # Easy stuff: one-to-one mapping from dict elements to
+            # instance attributes.
+            for key in (
+                'include_dirs',
+                'library_dirs',
+                'libraries',
+                'extra_objects',
+                'extra_compile_args',
+                'extra_link_args',
+            ):
+                val = build_info.get(key)
+                if val is not None:
+                    setattr(ext, key, val)
+
+            # Medium-easy stuff: same syntax/semantics, different names.
+            ext.runtime_library_dirs = build_info.get('rpath')
+            if 'def_file' in build_info:
+                log.warning("'def_file' element of build info dict no longer supported")
+
+            # Non-trivial stuff: 'macros' split into 'define_macros'
+            # and 'undef_macros'.
+            macros = build_info.get('macros')
+            if macros:
+                ext.define_macros = []
+                ext.undef_macros = []
+                for macro in macros:
+                    if not (isinstance(macro, tuple) and len(macro) in (1, 2)):
+                        raise DistutilsSetupError(
+                            "'macros' element of build info dict must be 1- or 2-tuple"
+                        )
+                    if len(macro) == 1:
+                        ext.undef_macros.append(macro[0])
+                    elif len(macro) == 2:
+                        ext.define_macros.append(macro)
+
+            extensions[i] = ext
+
+    def get_source_files(self):
+        self.check_extensions_list(self.extensions)
+        filenames = []
+
+        # Wouldn't it be neat if we knew the names of header files too...
+        for ext in self.extensions:
+            filenames.extend(ext.sources)
+        return filenames
+
+    def get_outputs(self):
+        # Sanity check the 'extensions' list -- can't assume this is being
+        # done in the same run as a 'build_extensions()' call (in fact, we
+        # can probably assume that it *isn't*!).
+        self.check_extensions_list(self.extensions)
+
+        # And build the list of output (built) filenames.  Note that this
+        # ignores the 'inplace' flag, and assumes everything goes in the
+        # "build" tree.
+        return [self.get_ext_fullpath(ext.name) for ext in self.extensions]
+
+    def build_extensions(self) -> None:
+        # First, sanity-check the 'extensions' list
+        self.check_extensions_list(self.extensions)
+        if self.parallel:
+            self._build_extensions_parallel()
+        else:
+            self._build_extensions_serial()
+
+    def _build_extensions_parallel(self):
+        workers = self.parallel
+        if self.parallel is True:
+            workers = os.cpu_count()  # may return None
+        try:
+            from concurrent.futures import ThreadPoolExecutor
+        except ImportError:
+            workers = None
+
+        if workers is None:
+            self._build_extensions_serial()
+            return
+
+        with ThreadPoolExecutor(max_workers=workers) as executor:
+            futures = [
+                executor.submit(self.build_extension, ext) for ext in self.extensions
+            ]
+            for ext, fut in zip(self.extensions, futures):
+                with self._filter_build_errors(ext):
+                    fut.result()
+
+    def _build_extensions_serial(self):
+        for ext in self.extensions:
+            with self._filter_build_errors(ext):
+                self.build_extension(ext)
+
+    @contextlib.contextmanager
+    def _filter_build_errors(self, ext):
+        try:
+            yield
+        except (CCompilerError, DistutilsError, CompileError) as e:
+            if not ext.optional:
+                raise
+            self.warn(f'building extension "{ext.name}" failed: {e}')
+
+    def build_extension(self, ext) -> None:
+        sources = ext.sources
+        if sources is None or not isinstance(sources, (list, tuple)):
+            raise DistutilsSetupError(
+                f"in 'ext_modules' option (extension '{ext.name}'), "
+                "'sources' must be present and must be "
+                "a list of source filenames"
+            )
+        # sort to make the resulting .so file build reproducible
+        sources = sorted(sources)
+
+        ext_path = self.get_ext_fullpath(ext.name)
+        depends = sources + ext.depends
+        if not (self.force or newer_group(depends, ext_path, 'newer')):
+            log.debug("skipping '%s' extension (up-to-date)", ext.name)
+            return
+        else:
+            log.info("building '%s' extension", ext.name)
+
+        # First, scan the sources for SWIG definition files (.i), run
+        # SWIG on 'em to create .c files, and modify the sources list
+        # accordingly.
+        sources = self.swig_sources(sources, ext)
+
+        # Next, compile the source code to object files.
+
+        # XXX not honouring 'define_macros' or 'undef_macros' -- the
+        # CCompiler API needs to change to accommodate this, and I
+        # want to do one thing at a time!
+
+        # Two possible sources for extra compiler arguments:
+        #   - 'extra_compile_args' in Extension object
+        #   - CFLAGS environment variable (not particularly
+        #     elegant, but people seem to expect it and I
+        #     guess it's useful)
+        # The environment variable should take precedence, and
+        # any sensible compiler will give precedence to later
+        # command line args.  Hence we combine them in order:
+        extra_args = ext.extra_compile_args or []
+
+        macros = ext.define_macros[:]
+        for undef in ext.undef_macros:
+            macros.append((undef,))
+
+        objects = self.compiler.compile(
+            sources,
+            output_dir=self.build_temp,
+            macros=macros,
+            include_dirs=ext.include_dirs,
+            debug=self.debug,
+            extra_postargs=extra_args,
+            depends=ext.depends,
+        )
+
+        # XXX outdated variable, kept here in case third-part code
+        # needs it.
+        self._built_objects = objects[:]
+
+        # Now link the object files together into a "shared object" --
+        # of course, first we have to figure out all the other things
+        # that go into the mix.
+        if ext.extra_objects:
+            objects.extend(ext.extra_objects)
+        extra_args = ext.extra_link_args or []
+
+        # Detect target language, if not provided
+        language = ext.language or self.compiler.detect_language(sources)
+
+        self.compiler.link_shared_object(
+            objects,
+            ext_path,
+            libraries=self.get_libraries(ext),
+            library_dirs=ext.library_dirs,
+            runtime_library_dirs=ext.runtime_library_dirs,
+            extra_postargs=extra_args,
+            export_symbols=self.get_export_symbols(ext),
+            debug=self.debug,
+            build_temp=self.build_temp,
+            target_lang=language,
+        )
+
+    def swig_sources(self, sources, extension):
+        """Walk the list of source files in 'sources', looking for SWIG
+        interface (.i) files.  Run SWIG on all that are found, and
+        return a modified 'sources' list with SWIG source files replaced
+        by the generated C (or C++) files.
+        """
+        new_sources = []
+        swig_sources = []
+        swig_targets = {}
+
+        # XXX this drops generated C/C++ files into the source tree, which
+        # is fine for developers who want to distribute the generated
+        # source -- but there should be an option to put SWIG output in
+        # the temp dir.
+
+        if self.swig_cpp:
+            log.warning("--swig-cpp is deprecated - use --swig-opts=-c++")
+
+        if (
+            self.swig_cpp
+            or ('-c++' in self.swig_opts)
+            or ('-c++' in extension.swig_opts)
+        ):
+            target_ext = '.cpp'
+        else:
+            target_ext = '.c'
+
+        for source in sources:
+            (base, ext) = os.path.splitext(source)
+            if ext == ".i":  # SWIG interface file
+                new_sources.append(base + '_wrap' + target_ext)
+                swig_sources.append(source)
+                swig_targets[source] = new_sources[-1]
+            else:
+                new_sources.append(source)
+
+        if not swig_sources:
+            return new_sources
+
+        swig = self.swig or self.find_swig()
+        swig_cmd = [swig, "-python"]
+        swig_cmd.extend(self.swig_opts)
+        if self.swig_cpp:
+            swig_cmd.append("-c++")
+
+        # Do not override commandline arguments
+        if not self.swig_opts:
+            swig_cmd.extend(extension.swig_opts)
+
+        for source in swig_sources:
+            target = swig_targets[source]
+            log.info("swigging %s to %s", source, target)
+            self.spawn(swig_cmd + ["-o", target, source])
+
+        return new_sources
+
+    def find_swig(self):
+        """Return the name of the SWIG executable.  On Unix, this is
+        just "swig" -- it should be in the PATH.  Tries a bit harder on
+        Windows.
+        """
+        if os.name == "posix":
+            return "swig"
+        elif os.name == "nt":
+            # Look for SWIG in its standard installation directory on
+            # Windows (or so I presume!).  If we find it there, great;
+            # if not, act like Unix and assume it's in the PATH.
+            for vers in ("1.3", "1.2", "1.1"):
+                fn = os.path.join(f"c:\\swig{vers}", "swig.exe")
+                if os.path.isfile(fn):
+                    return fn
+            else:
+                return "swig.exe"
+        else:
+            raise DistutilsPlatformError(
+                f"I don't know how to find (much less run) SWIG on platform '{os.name}'"
+            )
+
+    # -- Name generators -----------------------------------------------
+    # (extension names, filenames, whatever)
+    def get_ext_fullpath(self, ext_name: str) -> str:
+        """Returns the path of the filename for a given extension.
+
+        The file is located in `build_lib` or directly in the package
+        (inplace option).
+        """
+        fullname = self.get_ext_fullname(ext_name)
+        modpath = fullname.split('.')
+        filename = self.get_ext_filename(modpath[-1])
+
+        if not self.inplace:
+            # no further work needed
+            # returning :
+            #   build_dir/package/path/filename
+            filename = os.path.join(*modpath[:-1] + [filename])
+            return os.path.join(self.build_lib, filename)
+
+        # the inplace option requires to find the package directory
+        # using the build_py command for that
+        package = '.'.join(modpath[0:-1])
+        build_py = self.get_finalized_command('build_py')
+        package_dir = os.path.abspath(build_py.get_package_dir(package))
+
+        # returning
+        #   package_dir/filename
+        return os.path.join(package_dir, filename)
+
+    def get_ext_fullname(self, ext_name: str) -> str:
+        """Returns the fullname of a given extension name.
+
+        Adds the `package.` prefix"""
+        if self.package is None:
+            return ext_name
+        else:
+            return self.package + '.' + ext_name
+
+    def get_ext_filename(self, ext_name: str) -> str:
+        r"""Convert the name of an extension (eg. "foo.bar") into the name
+        of the file from which it will be loaded (eg. "foo/bar.so", or
+        "foo\bar.pyd").
+        """
+        from ..sysconfig import get_config_var
+
+        ext_path = ext_name.split('.')
+        ext_suffix = get_config_var('EXT_SUFFIX')
+        return os.path.join(*ext_path) + ext_suffix
+
+    def get_export_symbols(self, ext: Extension) -> list[str]:
+        """Return the list of symbols that a shared extension has to
+        export.  This either uses 'ext.export_symbols' or, if it's not
+        provided, "PyInit_" + module_name.  Only relevant on Windows, where
+        the .pyd file (DLL) must export the module "PyInit_" function.
+        """
+        name = self._get_module_name_for_symbol(ext)
+        try:
+            # Unicode module name support as defined in PEP-489
+            # https://peps.python.org/pep-0489/#export-hook-name
+            name.encode('ascii')
+        except UnicodeEncodeError:
+            suffix = 'U_' + name.encode('punycode').replace(b'-', b'_').decode('ascii')
+        else:
+            suffix = "_" + name
+
+        initfunc_name = "PyInit" + suffix
+        if initfunc_name not in ext.export_symbols:
+            ext.export_symbols.append(initfunc_name)
+        return ext.export_symbols
+
+    def _get_module_name_for_symbol(self, ext):
+        # Package name should be used for `__init__` modules
+        # https://github.com/python/cpython/issues/80074
+        # https://github.com/pypa/setuptools/issues/4826
+        parts = ext.name.split(".")
+        if parts[-1] == "__init__" and len(parts) >= 2:
+            return parts[-2]
+        return parts[-1]
+
+    def get_libraries(self, ext: Extension) -> list[str]:  # noqa: C901
+        """Return the list of libraries to link against when building a
+        shared extension.  On most platforms, this is just 'ext.libraries';
+        on Windows, we add the Python library (eg. python20.dll).
+        """
+        # The python library is always needed on Windows.  For MSVC, this
+        # is redundant, since the library is mentioned in a pragma in
+        # pyconfig.h that MSVC groks.  The other Windows compilers all seem
+        # to need it mentioned explicitly, though, so that's what we do.
+        # Append '_d' to the python import library on debug builds.
+        if sys.platform == "win32" and not is_mingw():
+            from .._msvccompiler import MSVCCompiler
+
+            if not isinstance(self.compiler, MSVCCompiler):
+                template = "python%d%d"
+                if self.debug:
+                    template = template + '_d'
+                pythonlib = template % (
+                    sys.hexversion >> 24,
+                    (sys.hexversion >> 16) & 0xFF,
+                )
+                # don't extend ext.libraries, it may be shared with other
+                # extensions, it is a reference to the original list
+                return ext.libraries + [pythonlib]
+        else:
+            # On Android only the main executable and LD_PRELOADs are considered
+            # to be RTLD_GLOBAL, all the dependencies of the main executable
+            # remain RTLD_LOCAL and so the shared libraries must be linked with
+            # libpython when python is built with a shared python library (issue
+            # bpo-21536).
+            # On Cygwin (and if required, other POSIX-like platforms based on
+            # Windows like MinGW) it is simply necessary that all symbols in
+            # shared libraries are resolved at link time.
+            from ..sysconfig import get_config_var
+
+            link_libpython = False
+            if get_config_var('Py_ENABLE_SHARED'):
+                # A native build on an Android device or on Cygwin
+                if hasattr(sys, 'getandroidapilevel'):
+                    link_libpython = True
+                elif sys.platform == 'cygwin' or is_mingw():
+                    link_libpython = True
+                elif '_PYTHON_HOST_PLATFORM' in os.environ:
+                    # We are cross-compiling for one of the relevant platforms
+                    if get_config_var('ANDROID_API_LEVEL') != 0:
+                        link_libpython = True
+                    elif get_config_var('MACHDEP') == 'cygwin':
+                        link_libpython = True
+
+            if link_libpython:
+                ldversion = get_config_var('LDVERSION')
+                return ext.libraries + ['python' + ldversion]
+
+        return ext.libraries
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_py.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_py.py
new file mode 100644
index 00000000..a20b076f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_py.py
@@ -0,0 +1,407 @@
+"""distutils.command.build_py
+
+Implements the Distutils 'build_py' command."""
+
+import glob
+import importlib.util
+import os
+import sys
+from distutils._log import log
+from typing import ClassVar
+
+from ..core import Command
+from ..errors import DistutilsFileError, DistutilsOptionError
+from ..util import convert_path
+
+
+class build_py(Command):
+    description = "\"build\" pure Python modules (copy to build directory)"
+
+    user_options = [
+        ('build-lib=', 'd', "directory to \"build\" (copy) to"),
+        ('compile', 'c', "compile .py to .pyc"),
+        ('no-compile', None, "don't compile .py files [default]"),
+        (
+            'optimize=',
+            'O',
+            "also compile with optimization: -O1 for \"python -O\", "
+            "-O2 for \"python -OO\", and -O0 to disable [default: -O0]",
+        ),
+        ('force', 'f', "forcibly build everything (ignore file timestamps)"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['compile', 'force']
+    negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}
+
+    def initialize_options(self):
+        self.build_lib = None
+        self.py_modules = None
+        self.package = None
+        self.package_data = None
+        self.package_dir = None
+        self.compile = False
+        self.optimize = 0
+        self.force = None
+
+    def finalize_options(self) -> None:
+        self.set_undefined_options(
+            'build', ('build_lib', 'build_lib'), ('force', 'force')
+        )
+
+        # Get the distribution options that are aliases for build_py
+        # options -- list of packages and list of modules.
+        self.packages = self.distribution.packages
+        self.py_modules = self.distribution.py_modules
+        self.package_data = self.distribution.package_data
+        self.package_dir = {}
+        if self.distribution.package_dir:
+            for name, path in self.distribution.package_dir.items():
+                self.package_dir[name] = convert_path(path)
+        self.data_files = self.get_data_files()
+
+        # Ick, copied straight from install_lib.py (fancy_getopt needs a
+        # type system!  Hell, *everything* needs a type system!!!)
+        if not isinstance(self.optimize, int):
+            try:
+                self.optimize = int(self.optimize)
+                assert 0 <= self.optimize <= 2
+            except (ValueError, AssertionError):
+                raise DistutilsOptionError("optimize must be 0, 1, or 2")
+
+    def run(self) -> None:
+        # XXX copy_file by default preserves atime and mtime.  IMHO this is
+        # the right thing to do, but perhaps it should be an option -- in
+        # particular, a site administrator might want installed files to
+        # reflect the time of installation rather than the last
+        # modification time before the installed release.
+
+        # XXX copy_file by default preserves mode, which appears to be the
+        # wrong thing to do: if a file is read-only in the working
+        # directory, we want it to be installed read/write so that the next
+        # installation of the same module distribution can overwrite it
+        # without problems.  (This might be a Unix-specific issue.)  Thus
+        # we turn off 'preserve_mode' when copying to the build directory,
+        # since the build directory is supposed to be exactly what the
+        # installation will look like (ie. we preserve mode when
+        # installing).
+
+        # Two options control which modules will be installed: 'packages'
+        # and 'py_modules'.  The former lets us work with whole packages, not
+        # specifying individual modules at all; the latter is for
+        # specifying modules one-at-a-time.
+
+        if self.py_modules:
+            self.build_modules()
+        if self.packages:
+            self.build_packages()
+            self.build_package_data()
+
+        self.byte_compile(self.get_outputs(include_bytecode=False))
+
+    def get_data_files(self):
+        """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
+        data = []
+        if not self.packages:
+            return data
+        for package in self.packages:
+            # Locate package source directory
+            src_dir = self.get_package_dir(package)
+
+            # Compute package build directory
+            build_dir = os.path.join(*([self.build_lib] + package.split('.')))
+
+            # Length of path to strip from found files
+            plen = 0
+            if src_dir:
+                plen = len(src_dir) + 1
+
+            # Strip directory from globbed filenames
+            filenames = [file[plen:] for file in self.find_data_files(package, src_dir)]
+            data.append((package, src_dir, build_dir, filenames))
+        return data
+
+    def find_data_files(self, package, src_dir):
+        """Return filenames for package's data files in 'src_dir'"""
+        globs = self.package_data.get('', []) + self.package_data.get(package, [])
+        files = []
+        for pattern in globs:
+            # Each pattern has to be converted to a platform-specific path
+            filelist = glob.glob(
+                os.path.join(glob.escape(src_dir), convert_path(pattern))
+            )
+            # Files that match more than one pattern are only added once
+            files.extend([
+                fn for fn in filelist if fn not in files and os.path.isfile(fn)
+            ])
+        return files
+
+    def build_package_data(self) -> None:
+        """Copy data files into build directory"""
+        for _package, src_dir, build_dir, filenames in self.data_files:
+            for filename in filenames:
+                target = os.path.join(build_dir, filename)
+                self.mkpath(os.path.dirname(target))
+                self.copy_file(
+                    os.path.join(src_dir, filename), target, preserve_mode=False
+                )
+
+    def get_package_dir(self, package):
+        """Return the directory, relative to the top of the source
+        distribution, where package 'package' should be found
+        (at least according to the 'package_dir' option, if any)."""
+        path = package.split('.')
+
+        if not self.package_dir:
+            if path:
+                return os.path.join(*path)
+            else:
+                return ''
+        else:
+            tail = []
+            while path:
+                try:
+                    pdir = self.package_dir['.'.join(path)]
+                except KeyError:
+                    tail.insert(0, path[-1])
+                    del path[-1]
+                else:
+                    tail.insert(0, pdir)
+                    return os.path.join(*tail)
+            else:
+                # Oops, got all the way through 'path' without finding a
+                # match in package_dir.  If package_dir defines a directory
+                # for the root (nameless) package, then fallback on it;
+                # otherwise, we might as well have not consulted
+                # package_dir at all, as we just use the directory implied
+                # by 'tail' (which should be the same as the original value
+                # of 'path' at this point).
+                pdir = self.package_dir.get('')
+                if pdir is not None:
+                    tail.insert(0, pdir)
+
+                if tail:
+                    return os.path.join(*tail)
+                else:
+                    return ''
+
+    def check_package(self, package, package_dir):
+        # Empty dir name means current directory, which we can probably
+        # assume exists.  Also, os.path.exists and isdir don't know about
+        # my "empty string means current dir" convention, so we have to
+        # circumvent them.
+        if package_dir != "":
+            if not os.path.exists(package_dir):
+                raise DistutilsFileError(
+                    f"package directory '{package_dir}' does not exist"
+                )
+            if not os.path.isdir(package_dir):
+                raise DistutilsFileError(
+                    f"supposed package directory '{package_dir}' exists, "
+                    "but is not a directory"
+                )
+
+        # Directories without __init__.py are namespace packages (PEP 420).
+        if package:
+            init_py = os.path.join(package_dir, "__init__.py")
+            if os.path.isfile(init_py):
+                return init_py
+
+        # Either not in a package at all (__init__.py not expected), or
+        # __init__.py doesn't exist -- so don't return the filename.
+        return None
+
+    def check_module(self, module, module_file):
+        if not os.path.isfile(module_file):
+            log.warning("file %s (for module %s) not found", module_file, module)
+            return False
+        else:
+            return True
+
+    def find_package_modules(self, package, package_dir):
+        self.check_package(package, package_dir)
+        module_files = glob.glob(os.path.join(glob.escape(package_dir), "*.py"))
+        modules = []
+        setup_script = os.path.abspath(self.distribution.script_name)
+
+        for f in module_files:
+            abs_f = os.path.abspath(f)
+            if abs_f != setup_script:
+                module = os.path.splitext(os.path.basename(f))[0]
+                modules.append((package, module, f))
+            else:
+                self.debug_print(f"excluding {setup_script}")
+        return modules
+
+    def find_modules(self):
+        """Finds individually-specified Python modules, ie. those listed by
+        module name in 'self.py_modules'.  Returns a list of tuples (package,
+        module_base, filename): 'package' is a tuple of the path through
+        package-space to the module; 'module_base' is the bare (no
+        packages, no dots) module name, and 'filename' is the path to the
+        ".py" file (relative to the distribution root) that implements the
+        module.
+        """
+        # Map package names to tuples of useful info about the package:
+        #    (package_dir, checked)
+        # package_dir - the directory where we'll find source files for
+        #   this package
+        # checked - true if we have checked that the package directory
+        #   is valid (exists, contains __init__.py, ... ?)
+        packages = {}
+
+        # List of (package, module, filename) tuples to return
+        modules = []
+
+        # We treat modules-in-packages almost the same as toplevel modules,
+        # just the "package" for a toplevel is empty (either an empty
+        # string or empty list, depending on context).  Differences:
+        #   - don't check for __init__.py in directory for empty package
+        for module in self.py_modules:
+            path = module.split('.')
+            package = '.'.join(path[0:-1])
+            module_base = path[-1]
+
+            try:
+                (package_dir, checked) = packages[package]
+            except KeyError:
+                package_dir = self.get_package_dir(package)
+                checked = False
+
+            if not checked:
+                init_py = self.check_package(package, package_dir)
+                packages[package] = (package_dir, 1)
+                if init_py:
+                    modules.append((package, "__init__", init_py))
+
+            # XXX perhaps we should also check for just .pyc files
+            # (so greedy closed-source bastards can distribute Python
+            # modules too)
+            module_file = os.path.join(package_dir, module_base + ".py")
+            if not self.check_module(module, module_file):
+                continue
+
+            modules.append((package, module_base, module_file))
+
+        return modules
+
+    def find_all_modules(self):
+        """Compute the list of all modules that will be built, whether
+        they are specified one-module-at-a-time ('self.py_modules') or
+        by whole packages ('self.packages').  Return a list of tuples
+        (package, module, module_file), just like 'find_modules()' and
+        'find_package_modules()' do."""
+        modules = []
+        if self.py_modules:
+            modules.extend(self.find_modules())
+        if self.packages:
+            for package in self.packages:
+                package_dir = self.get_package_dir(package)
+                m = self.find_package_modules(package, package_dir)
+                modules.extend(m)
+        return modules
+
+    def get_source_files(self):
+        return [module[-1] for module in self.find_all_modules()]
+
+    def get_module_outfile(self, build_dir, package, module):
+        outfile_path = [build_dir] + list(package) + [module + ".py"]
+        return os.path.join(*outfile_path)
+
+    def get_outputs(self, include_bytecode: bool = True) -> list[str]:
+        modules = self.find_all_modules()
+        outputs = []
+        for package, module, _module_file in modules:
+            package = package.split('.')
+            filename = self.get_module_outfile(self.build_lib, package, module)
+            outputs.append(filename)
+            if include_bytecode:
+                if self.compile:
+                    outputs.append(
+                        importlib.util.cache_from_source(filename, optimization='')
+                    )
+                if self.optimize > 0:
+                    outputs.append(
+                        importlib.util.cache_from_source(
+                            filename, optimization=self.optimize
+                        )
+                    )
+
+        outputs += [
+            os.path.join(build_dir, filename)
+            for package, src_dir, build_dir, filenames in self.data_files
+            for filename in filenames
+        ]
+
+        return outputs
+
+    def build_module(self, module, module_file, package):
+        if isinstance(package, str):
+            package = package.split('.')
+        elif not isinstance(package, (list, tuple)):
+            raise TypeError(
+                "'package' must be a string (dot-separated), list, or tuple"
+            )
+
+        # Now put the module source file into the "build" area -- this is
+        # easy, we just copy it somewhere under self.build_lib (the build
+        # directory for Python source).
+        outfile = self.get_module_outfile(self.build_lib, package, module)
+        dir = os.path.dirname(outfile)
+        self.mkpath(dir)
+        return self.copy_file(module_file, outfile, preserve_mode=False)
+
+    def build_modules(self) -> None:
+        modules = self.find_modules()
+        for package, module, module_file in modules:
+            # Now "build" the module -- ie. copy the source file to
+            # self.build_lib (the build directory for Python source).
+            # (Actually, it gets copied to the directory for this package
+            # under self.build_lib.)
+            self.build_module(module, module_file, package)
+
+    def build_packages(self) -> None:
+        for package in self.packages:
+            # Get list of (package, module, module_file) tuples based on
+            # scanning the package directory.  'package' is only included
+            # in the tuple so that 'find_modules()' and
+            # 'find_package_tuples()' have a consistent interface; it's
+            # ignored here (apart from a sanity check).  Also, 'module' is
+            # the *unqualified* module name (ie. no dots, no package -- we
+            # already know its package!), and 'module_file' is the path to
+            # the .py file, relative to the current directory
+            # (ie. including 'package_dir').
+            package_dir = self.get_package_dir(package)
+            modules = self.find_package_modules(package, package_dir)
+
+            # Now loop over the modules we found, "building" each one (just
+            # copy it to self.build_lib).
+            for package_, module, module_file in modules:
+                assert package == package_
+                self.build_module(module, module_file, package)
+
+    def byte_compile(self, files) -> None:
+        if sys.dont_write_bytecode:
+            self.warn('byte-compiling is disabled, skipping.')
+            return
+
+        from ..util import byte_compile
+
+        prefix = self.build_lib
+        if prefix[-1] != os.sep:
+            prefix = prefix + os.sep
+
+        # XXX this code is essentially the same as the 'byte_compile()
+        # method of the "install_lib" command, except for the determination
+        # of the 'prefix' string.  Hmmm.
+        if self.compile:
+            byte_compile(
+                files, optimize=0, force=self.force, prefix=prefix, dry_run=self.dry_run
+            )
+        if self.optimize > 0:
+            byte_compile(
+                files,
+                optimize=self.optimize,
+                force=self.force,
+                prefix=prefix,
+                dry_run=self.dry_run,
+            )
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_scripts.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_scripts.py
new file mode 100644
index 00000000..127c51d8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/build_scripts.py
@@ -0,0 +1,160 @@
+"""distutils.command.build_scripts
+
+Implements the Distutils 'build_scripts' command."""
+
+import os
+import re
+import tokenize
+from distutils._log import log
+from stat import ST_MODE
+from typing import ClassVar
+
+from .._modified import newer
+from ..core import Command
+from ..util import convert_path
+
+shebang_pattern = re.compile('^#!.*python[0-9.]*([ \t].*)?$')
+"""
+Pattern matching a Python interpreter indicated in first line of a script.
+"""
+
+# for Setuptools compatibility
+first_line_re = shebang_pattern
+
+
+class build_scripts(Command):
+    description = "\"build\" scripts (copy and fixup #! line)"
+
+    user_options: ClassVar[list[tuple[str, str, str]]] = [
+        ('build-dir=', 'd', "directory to \"build\" (copy) to"),
+        ('force', 'f', "forcibly build everything (ignore file timestamps"),
+        ('executable=', 'e', "specify final destination interpreter path"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['force']
+
+    def initialize_options(self):
+        self.build_dir = None
+        self.scripts = None
+        self.force = None
+        self.executable = None
+
+    def finalize_options(self):
+        self.set_undefined_options(
+            'build',
+            ('build_scripts', 'build_dir'),
+            ('force', 'force'),
+            ('executable', 'executable'),
+        )
+        self.scripts = self.distribution.scripts
+
+    def get_source_files(self):
+        return self.scripts
+
+    def run(self):
+        if not self.scripts:
+            return
+        self.copy_scripts()
+
+    def copy_scripts(self):
+        """
+        Copy each script listed in ``self.scripts``.
+
+        If a script is marked as a Python script (first line matches
+        'shebang_pattern', i.e. starts with ``#!`` and contains
+        "python"), then adjust in the copy the first line to refer to
+        the current Python interpreter.
+        """
+        self.mkpath(self.build_dir)
+        outfiles = []
+        updated_files = []
+        for script in self.scripts:
+            self._copy_script(script, outfiles, updated_files)
+
+        self._change_modes(outfiles)
+
+        return outfiles, updated_files
+
+    def _copy_script(self, script, outfiles, updated_files):
+        shebang_match = None
+        script = convert_path(script)
+        outfile = os.path.join(self.build_dir, os.path.basename(script))
+        outfiles.append(outfile)
+
+        if not self.force and not newer(script, outfile):
+            log.debug("not copying %s (up-to-date)", script)
+            return
+
+        # Always open the file, but ignore failures in dry-run mode
+        # in order to attempt to copy directly.
+        try:
+            f = tokenize.open(script)
+        except OSError:
+            if not self.dry_run:
+                raise
+            f = None
+        else:
+            first_line = f.readline()
+            if not first_line:
+                self.warn(f"{script} is an empty file (skipping)")
+                return
+
+            shebang_match = shebang_pattern.match(first_line)
+
+        updated_files.append(outfile)
+        if shebang_match:
+            log.info("copying and adjusting %s -> %s", script, self.build_dir)
+            if not self.dry_run:
+                post_interp = shebang_match.group(1) or ''
+                shebang = f"#!python{post_interp}\n"
+                self._validate_shebang(shebang, f.encoding)
+                with open(outfile, "w", encoding=f.encoding) as outf:
+                    outf.write(shebang)
+                    outf.writelines(f.readlines())
+            if f:
+                f.close()
+        else:
+            if f:
+                f.close()
+            self.copy_file(script, outfile)
+
+    def _change_modes(self, outfiles):
+        if os.name != 'posix':
+            return
+
+        for file in outfiles:
+            self._change_mode(file)
+
+    def _change_mode(self, file):
+        if self.dry_run:
+            log.info("changing mode of %s", file)
+            return
+
+        oldmode = os.stat(file)[ST_MODE] & 0o7777
+        newmode = (oldmode | 0o555) & 0o7777
+        if newmode != oldmode:
+            log.info("changing mode of %s from %o to %o", file, oldmode, newmode)
+            os.chmod(file, newmode)
+
+    @staticmethod
+    def _validate_shebang(shebang, encoding):
+        # Python parser starts to read a script using UTF-8 until
+        # it gets a #coding:xxx cookie. The shebang has to be the
+        # first line of a file, the #coding:xxx cookie cannot be
+        # written before. So the shebang has to be encodable to
+        # UTF-8.
+        try:
+            shebang.encode('utf-8')
+        except UnicodeEncodeError:
+            raise ValueError(f"The shebang ({shebang!r}) is not encodable to utf-8")
+
+        # If the script is encoded to a custom encoding (use a
+        # #coding:xxx cookie), the shebang has to be encodable to
+        # the script encoding too.
+        try:
+            shebang.encode(encoding)
+        except UnicodeEncodeError:
+            raise ValueError(
+                f"The shebang ({shebang!r}) is not encodable "
+                f"to the script encoding ({encoding})"
+            )
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/check.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/check.py
new file mode 100644
index 00000000..58a823dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/check.py
@@ -0,0 +1,152 @@
+"""distutils.command.check
+
+Implements the Distutils 'check' command.
+"""
+
+import contextlib
+from typing import ClassVar
+
+from ..core import Command
+from ..errors import DistutilsSetupError
+
+with contextlib.suppress(ImportError):
+    import docutils.frontend
+    import docutils.nodes
+    import docutils.parsers.rst
+    import docutils.utils
+
+    class SilentReporter(docutils.utils.Reporter):
+        def __init__(
+            self,
+            source,
+            report_level,
+            halt_level,
+            stream=None,
+            debug=False,
+            encoding='ascii',
+            error_handler='replace',
+        ):
+            self.messages = []
+            super().__init__(
+                source, report_level, halt_level, stream, debug, encoding, error_handler
+            )
+
+        def system_message(self, level, message, *children, **kwargs):
+            self.messages.append((level, message, children, kwargs))
+            return docutils.nodes.system_message(
+                message, *children, level=level, type=self.levels[level], **kwargs
+            )
+
+
+class check(Command):
+    """This command checks the meta-data of the package."""
+
+    description = "perform some checks on the package"
+    user_options: ClassVar[list[tuple[str, str, str]]] = [
+        ('metadata', 'm', 'Verify meta-data'),
+        (
+            'restructuredtext',
+            'r',
+            'Checks if long string meta-data syntax are reStructuredText-compliant',
+        ),
+        ('strict', 's', 'Will exit with an error if a check fails'),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['metadata', 'restructuredtext', 'strict']
+
+    def initialize_options(self):
+        """Sets default values for options."""
+        self.restructuredtext = False
+        self.metadata = 1
+        self.strict = False
+        self._warnings = 0
+
+    def finalize_options(self):
+        pass
+
+    def warn(self, msg):
+        """Counts the number of warnings that occurs."""
+        self._warnings += 1
+        return Command.warn(self, msg)
+
+    def run(self):
+        """Runs the command."""
+        # perform the various tests
+        if self.metadata:
+            self.check_metadata()
+        if self.restructuredtext:
+            if 'docutils' in globals():
+                try:
+                    self.check_restructuredtext()
+                except TypeError as exc:
+                    raise DistutilsSetupError(str(exc))
+            elif self.strict:
+                raise DistutilsSetupError('The docutils package is needed.')
+
+        # let's raise an error in strict mode, if we have at least
+        # one warning
+        if self.strict and self._warnings > 0:
+            raise DistutilsSetupError('Please correct your package.')
+
+    def check_metadata(self):
+        """Ensures that all required elements of meta-data are supplied.
+
+        Required fields:
+            name, version
+
+        Warns if any are missing.
+        """
+        metadata = self.distribution.metadata
+
+        missing = [
+            attr for attr in ('name', 'version') if not getattr(metadata, attr, None)
+        ]
+
+        if missing:
+            self.warn("missing required meta-data: {}".format(', '.join(missing)))
+
+    def check_restructuredtext(self):
+        """Checks if the long string fields are reST-compliant."""
+        data = self.distribution.get_long_description()
+        for warning in self._check_rst_data(data):
+            line = warning[-1].get('line')
+            if line is None:
+                warning = warning[1]
+            else:
+                warning = f'{warning[1]} (line {line})'
+            self.warn(warning)
+
+    def _check_rst_data(self, data):
+        """Returns warnings when the provided data doesn't compile."""
+        # the include and csv_table directives need this to be a path
+        source_path = self.distribution.script_name or 'setup.py'
+        parser = docutils.parsers.rst.Parser()
+        settings = docutils.frontend.OptionParser(
+            components=(docutils.parsers.rst.Parser,)
+        ).get_default_values()
+        settings.tab_width = 4
+        settings.pep_references = None
+        settings.rfc_references = None
+        reporter = SilentReporter(
+            source_path,
+            settings.report_level,
+            settings.halt_level,
+            stream=settings.warning_stream,
+            debug=settings.debug,
+            encoding=settings.error_encoding,
+            error_handler=settings.error_encoding_error_handler,
+        )
+
+        document = docutils.nodes.document(settings, reporter, source=source_path)
+        document.note_source(source_path, -1)
+        try:
+            parser.parse(data, document)
+        except (AttributeError, TypeError) as e:
+            reporter.messages.append((
+                -1,
+                f'Could not finish the parsing: {e}.',
+                '',
+                {},
+            ))
+
+        return reporter.messages
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/clean.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/clean.py
new file mode 100644
index 00000000..23427aba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/clean.py
@@ -0,0 +1,77 @@
+"""distutils.command.clean
+
+Implements the Distutils 'clean' command."""
+
+# contributed by Bastian Kleineidam <calvin@cs.uni-sb.de>, added 2000-03-18
+
+import os
+from distutils._log import log
+from typing import ClassVar
+
+from ..core import Command
+from ..dir_util import remove_tree
+
+
+class clean(Command):
+    description = "clean up temporary files from 'build' command"
+    user_options = [
+        ('build-base=', 'b', "base build directory [default: 'build.build-base']"),
+        (
+            'build-lib=',
+            None,
+            "build directory for all modules [default: 'build.build-lib']",
+        ),
+        ('build-temp=', 't', "temporary build directory [default: 'build.build-temp']"),
+        (
+            'build-scripts=',
+            None,
+            "build directory for scripts [default: 'build.build-scripts']",
+        ),
+        ('bdist-base=', None, "temporary directory for built distributions"),
+        ('all', 'a', "remove all build output, not just temporary by-products"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['all']
+
+    def initialize_options(self):
+        self.build_base = None
+        self.build_lib = None
+        self.build_temp = None
+        self.build_scripts = None
+        self.bdist_base = None
+        self.all = None
+
+    def finalize_options(self):
+        self.set_undefined_options(
+            'build',
+            ('build_base', 'build_base'),
+            ('build_lib', 'build_lib'),
+            ('build_scripts', 'build_scripts'),
+            ('build_temp', 'build_temp'),
+        )
+        self.set_undefined_options('bdist', ('bdist_base', 'bdist_base'))
+
+    def run(self):
+        # remove the build/temp.<plat> directory (unless it's already
+        # gone)
+        if os.path.exists(self.build_temp):
+            remove_tree(self.build_temp, dry_run=self.dry_run)
+        else:
+            log.debug("'%s' does not exist -- can't clean it", self.build_temp)
+
+        if self.all:
+            # remove build directories
+            for directory in (self.build_lib, self.bdist_base, self.build_scripts):
+                if os.path.exists(directory):
+                    remove_tree(directory, dry_run=self.dry_run)
+                else:
+                    log.warning("'%s' does not exist -- can't clean it", directory)
+
+        # just for the heck of it, try to remove the base build directory:
+        # we might have emptied it right now, but if not we don't care
+        if not self.dry_run:
+            try:
+                os.rmdir(self.build_base)
+                log.info("removing '%s'", self.build_base)
+            except OSError:
+                pass
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/config.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/config.py
new file mode 100644
index 00000000..44df8233
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/config.py
@@ -0,0 +1,360 @@
+"""distutils.command.config
+
+Implements the Distutils 'config' command, a (mostly) empty command class
+that exists mainly to be sub-classed by specific module distributions and
+applications.  The idea is that while every "config" command is different,
+at least they're all named the same, and users always see "config" in the
+list of standard commands.  Also, this is a good place to put common
+configure-like tasks: "try to compile this C code", or "figure out where
+this header file lives".
+"""
+
+from __future__ import annotations
+
+import os
+import pathlib
+import re
+from collections.abc import Sequence
+from distutils._log import log
+
+from ..ccompiler import CCompiler, CompileError, LinkError, new_compiler
+from ..core import Command
+from ..errors import DistutilsExecError
+from ..sysconfig import customize_compiler
+
+LANG_EXT = {"c": ".c", "c++": ".cxx"}
+
+
+class config(Command):
+    description = "prepare to build"
+
+    user_options = [
+        ('compiler=', None, "specify the compiler type"),
+        ('cc=', None, "specify the compiler executable"),
+        ('include-dirs=', 'I', "list of directories to search for header files"),
+        ('define=', 'D', "C preprocessor macros to define"),
+        ('undef=', 'U', "C preprocessor macros to undefine"),
+        ('libraries=', 'l', "external C libraries to link with"),
+        ('library-dirs=', 'L', "directories to search for external C libraries"),
+        ('noisy', None, "show every action (compile, link, run, ...) taken"),
+        (
+            'dump-source',
+            None,
+            "dump generated source files before attempting to compile them",
+        ),
+    ]
+
+    # The three standard command methods: since the "config" command
+    # does nothing by default, these are empty.
+
+    def initialize_options(self):
+        self.compiler = None
+        self.cc = None
+        self.include_dirs = None
+        self.libraries = None
+        self.library_dirs = None
+
+        # maximal output for now
+        self.noisy = 1
+        self.dump_source = 1
+
+        # list of temporary files generated along-the-way that we have
+        # to clean at some point
+        self.temp_files = []
+
+    def finalize_options(self):
+        if self.include_dirs is None:
+            self.include_dirs = self.distribution.include_dirs or []
+        elif isinstance(self.include_dirs, str):
+            self.include_dirs = self.include_dirs.split(os.pathsep)
+
+        if self.libraries is None:
+            self.libraries = []
+        elif isinstance(self.libraries, str):
+            self.libraries = [self.libraries]
+
+        if self.library_dirs is None:
+            self.library_dirs = []
+        elif isinstance(self.library_dirs, str):
+            self.library_dirs = self.library_dirs.split(os.pathsep)
+
+    def run(self):
+        pass
+
+    # Utility methods for actual "config" commands.  The interfaces are
+    # loosely based on Autoconf macros of similar names.  Sub-classes
+    # may use these freely.
+
+    def _check_compiler(self):
+        """Check that 'self.compiler' really is a CCompiler object;
+        if not, make it one.
+        """
+        # We do this late, and only on-demand, because this is an expensive
+        # import.
+        if not isinstance(self.compiler, CCompiler):
+            self.compiler = new_compiler(
+                compiler=self.compiler, dry_run=self.dry_run, force=True
+            )
+            customize_compiler(self.compiler)
+            if self.include_dirs:
+                self.compiler.set_include_dirs(self.include_dirs)
+            if self.libraries:
+                self.compiler.set_libraries(self.libraries)
+            if self.library_dirs:
+                self.compiler.set_library_dirs(self.library_dirs)
+
+    def _gen_temp_sourcefile(self, body, headers, lang):
+        filename = "_configtest" + LANG_EXT[lang]
+        with open(filename, "w", encoding='utf-8') as file:
+            if headers:
+                for header in headers:
+                    file.write(f"#include <{header}>\n")
+                file.write("\n")
+            file.write(body)
+            if body[-1] != "\n":
+                file.write("\n")
+        return filename
+
+    def _preprocess(self, body, headers, include_dirs, lang):
+        src = self._gen_temp_sourcefile(body, headers, lang)
+        out = "_configtest.i"
+        self.temp_files.extend([src, out])
+        self.compiler.preprocess(src, out, include_dirs=include_dirs)
+        return (src, out)
+
+    def _compile(self, body, headers, include_dirs, lang):
+        src = self._gen_temp_sourcefile(body, headers, lang)
+        if self.dump_source:
+            dump_file(src, f"compiling '{src}':")
+        (obj,) = self.compiler.object_filenames([src])
+        self.temp_files.extend([src, obj])
+        self.compiler.compile([src], include_dirs=include_dirs)
+        return (src, obj)
+
+    def _link(self, body, headers, include_dirs, libraries, library_dirs, lang):
+        (src, obj) = self._compile(body, headers, include_dirs, lang)
+        prog = os.path.splitext(os.path.basename(src))[0]
+        self.compiler.link_executable(
+            [obj],
+            prog,
+            libraries=libraries,
+            library_dirs=library_dirs,
+            target_lang=lang,
+        )
+
+        if self.compiler.exe_extension is not None:
+            prog = prog + self.compiler.exe_extension
+        self.temp_files.append(prog)
+
+        return (src, obj, prog)
+
+    def _clean(self, *filenames):
+        if not filenames:
+            filenames = self.temp_files
+            self.temp_files = []
+        log.info("removing: %s", ' '.join(filenames))
+        for filename in filenames:
+            try:
+                os.remove(filename)
+            except OSError:
+                pass
+
+    # XXX these ignore the dry-run flag: what to do, what to do? even if
+    # you want a dry-run build, you still need some sort of configuration
+    # info.  My inclination is to make it up to the real config command to
+    # consult 'dry_run', and assume a default (minimal) configuration if
+    # true.  The problem with trying to do it here is that you'd have to
+    # return either true or false from all the 'try' methods, neither of
+    # which is correct.
+
+    # XXX need access to the header search path and maybe default macros.
+
+    def try_cpp(self, body=None, headers=None, include_dirs=None, lang="c"):
+        """Construct a source file from 'body' (a string containing lines
+        of C/C++ code) and 'headers' (a list of header files to include)
+        and run it through the preprocessor.  Return true if the
+        preprocessor succeeded, false if there were any errors.
+        ('body' probably isn't of much use, but what the heck.)
+        """
+        self._check_compiler()
+        ok = True
+        try:
+            self._preprocess(body, headers, include_dirs, lang)
+        except CompileError:
+            ok = False
+
+        self._clean()
+        return ok
+
+    def search_cpp(self, pattern, body=None, headers=None, include_dirs=None, lang="c"):
+        """Construct a source file (just like 'try_cpp()'), run it through
+        the preprocessor, and return true if any line of the output matches
+        'pattern'.  'pattern' should either be a compiled regex object or a
+        string containing a regex.  If both 'body' and 'headers' are None,
+        preprocesses an empty file -- which can be useful to determine the
+        symbols the preprocessor and compiler set by default.
+        """
+        self._check_compiler()
+        src, out = self._preprocess(body, headers, include_dirs, lang)
+
+        if isinstance(pattern, str):
+            pattern = re.compile(pattern)
+
+        with open(out, encoding='utf-8') as file:
+            match = any(pattern.search(line) for line in file)
+
+        self._clean()
+        return match
+
+    def try_compile(self, body, headers=None, include_dirs=None, lang="c"):
+        """Try to compile a source file built from 'body' and 'headers'.
+        Return true on success, false otherwise.
+        """
+        self._check_compiler()
+        try:
+            self._compile(body, headers, include_dirs, lang)
+            ok = True
+        except CompileError:
+            ok = False
+
+        log.info(ok and "success!" or "failure.")
+        self._clean()
+        return ok
+
+    def try_link(
+        self,
+        body,
+        headers=None,
+        include_dirs=None,
+        libraries=None,
+        library_dirs=None,
+        lang="c",
+    ):
+        """Try to compile and link a source file, built from 'body' and
+        'headers', to executable form.  Return true on success, false
+        otherwise.
+        """
+        self._check_compiler()
+        try:
+            self._link(body, headers, include_dirs, libraries, library_dirs, lang)
+            ok = True
+        except (CompileError, LinkError):
+            ok = False
+
+        log.info(ok and "success!" or "failure.")
+        self._clean()
+        return ok
+
+    def try_run(
+        self,
+        body,
+        headers=None,
+        include_dirs=None,
+        libraries=None,
+        library_dirs=None,
+        lang="c",
+    ):
+        """Try to compile, link to an executable, and run a program
+        built from 'body' and 'headers'.  Return true on success, false
+        otherwise.
+        """
+        self._check_compiler()
+        try:
+            src, obj, exe = self._link(
+                body, headers, include_dirs, libraries, library_dirs, lang
+            )
+            self.spawn([exe])
+            ok = True
+        except (CompileError, LinkError, DistutilsExecError):
+            ok = False
+
+        log.info(ok and "success!" or "failure.")
+        self._clean()
+        return ok
+
+    # -- High-level methods --------------------------------------------
+    # (these are the ones that are actually likely to be useful
+    # when implementing a real-world config command!)
+
+    def check_func(
+        self,
+        func,
+        headers=None,
+        include_dirs=None,
+        libraries=None,
+        library_dirs=None,
+        decl=False,
+        call=False,
+    ):
+        """Determine if function 'func' is available by constructing a
+        source file that refers to 'func', and compiles and links it.
+        If everything succeeds, returns true; otherwise returns false.
+
+        The constructed source file starts out by including the header
+        files listed in 'headers'.  If 'decl' is true, it then declares
+        'func' (as "int func()"); you probably shouldn't supply 'headers'
+        and set 'decl' true in the same call, or you might get errors about
+        a conflicting declarations for 'func'.  Finally, the constructed
+        'main()' function either references 'func' or (if 'call' is true)
+        calls it.  'libraries' and 'library_dirs' are used when
+        linking.
+        """
+        self._check_compiler()
+        body = []
+        if decl:
+            body.append(f"int {func} ();")
+        body.append("int main () {")
+        if call:
+            body.append(f"  {func}();")
+        else:
+            body.append(f"  {func};")
+        body.append("}")
+        body = "\n".join(body) + "\n"
+
+        return self.try_link(body, headers, include_dirs, libraries, library_dirs)
+
+    def check_lib(
+        self,
+        library,
+        library_dirs=None,
+        headers=None,
+        include_dirs=None,
+        other_libraries: Sequence[str] = [],
+    ):
+        """Determine if 'library' is available to be linked against,
+        without actually checking that any particular symbols are provided
+        by it.  'headers' will be used in constructing the source file to
+        be compiled, but the only effect of this is to check if all the
+        header files listed are available.  Any libraries listed in
+        'other_libraries' will be included in the link, in case 'library'
+        has symbols that depend on other libraries.
+        """
+        self._check_compiler()
+        return self.try_link(
+            "int main (void) { }",
+            headers,
+            include_dirs,
+            [library] + list(other_libraries),
+            library_dirs,
+        )
+
+    def check_header(self, header, include_dirs=None, library_dirs=None, lang="c"):
+        """Determine if the system header file named by 'header_file'
+        exists and can be found by the preprocessor; return true if so,
+        false otherwise.
+        """
+        return self.try_cpp(
+            body="/* No body */", headers=[header], include_dirs=include_dirs
+        )
+
+
+def dump_file(filename, head=None):
+    """Dumps a file content into log.info.
+
+    If head is not None, will be dumped before the file content.
+    """
+    if head is None:
+        log.info('%s', filename)
+    else:
+        log.info(head)
+    log.info(pathlib.Path(filename).read_text(encoding='utf-8'))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install.py
new file mode 100644
index 00000000..dc17e56a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install.py
@@ -0,0 +1,805 @@
+"""distutils.command.install
+
+Implements the Distutils 'install' command."""
+
+from __future__ import annotations
+
+import collections
+import contextlib
+import itertools
+import os
+import sys
+import sysconfig
+from distutils._log import log
+from site import USER_BASE, USER_SITE
+from typing import ClassVar
+
+from ..core import Command
+from ..debug import DEBUG
+from ..errors import DistutilsOptionError, DistutilsPlatformError
+from ..file_util import write_file
+from ..sysconfig import get_config_vars
+from ..util import change_root, convert_path, get_platform, subst_vars
+from . import _framework_compat as fw
+
+HAS_USER_SITE = True
+
+WINDOWS_SCHEME = {
+    'purelib': '{base}/Lib/site-packages',
+    'platlib': '{base}/Lib/site-packages',
+    'headers': '{base}/Include/{dist_name}',
+    'scripts': '{base}/Scripts',
+    'data': '{base}',
+}
+
+INSTALL_SCHEMES = {
+    'posix_prefix': {
+        'purelib': '{base}/lib/{implementation_lower}{py_version_short}/site-packages',
+        'platlib': '{platbase}/{platlibdir}/{implementation_lower}'
+        '{py_version_short}/site-packages',
+        'headers': '{base}/include/{implementation_lower}'
+        '{py_version_short}{abiflags}/{dist_name}',
+        'scripts': '{base}/bin',
+        'data': '{base}',
+    },
+    'posix_home': {
+        'purelib': '{base}/lib/{implementation_lower}',
+        'platlib': '{base}/{platlibdir}/{implementation_lower}',
+        'headers': '{base}/include/{implementation_lower}/{dist_name}',
+        'scripts': '{base}/bin',
+        'data': '{base}',
+    },
+    'nt': WINDOWS_SCHEME,
+    'pypy': {
+        'purelib': '{base}/site-packages',
+        'platlib': '{base}/site-packages',
+        'headers': '{base}/include/{dist_name}',
+        'scripts': '{base}/bin',
+        'data': '{base}',
+    },
+    'pypy_nt': {
+        'purelib': '{base}/site-packages',
+        'platlib': '{base}/site-packages',
+        'headers': '{base}/include/{dist_name}',
+        'scripts': '{base}/Scripts',
+        'data': '{base}',
+    },
+}
+
+# user site schemes
+if HAS_USER_SITE:
+    INSTALL_SCHEMES['nt_user'] = {
+        'purelib': '{usersite}',
+        'platlib': '{usersite}',
+        'headers': '{userbase}/{implementation}{py_version_nodot_plat}'
+        '/Include/{dist_name}',
+        'scripts': '{userbase}/{implementation}{py_version_nodot_plat}/Scripts',
+        'data': '{userbase}',
+    }
+
+    INSTALL_SCHEMES['posix_user'] = {
+        'purelib': '{usersite}',
+        'platlib': '{usersite}',
+        'headers': '{userbase}/include/{implementation_lower}'
+        '{py_version_short}{abiflags}/{dist_name}',
+        'scripts': '{userbase}/bin',
+        'data': '{userbase}',
+    }
+
+
+INSTALL_SCHEMES.update(fw.schemes)
+
+
+# The keys to an installation scheme; if any new types of files are to be
+# installed, be sure to add an entry to every installation scheme above,
+# and to SCHEME_KEYS here.
+SCHEME_KEYS = ('purelib', 'platlib', 'headers', 'scripts', 'data')
+
+
+def _load_sysconfig_schemes():
+    with contextlib.suppress(AttributeError):
+        return {
+            scheme: sysconfig.get_paths(scheme, expand=False)
+            for scheme in sysconfig.get_scheme_names()
+        }
+
+
+def _load_schemes():
+    """
+    Extend default schemes with schemes from sysconfig.
+    """
+
+    sysconfig_schemes = _load_sysconfig_schemes() or {}
+
+    return {
+        scheme: {
+            **INSTALL_SCHEMES.get(scheme, {}),
+            **sysconfig_schemes.get(scheme, {}),
+        }
+        for scheme in set(itertools.chain(INSTALL_SCHEMES, sysconfig_schemes))
+    }
+
+
+def _get_implementation():
+    if hasattr(sys, 'pypy_version_info'):
+        return 'PyPy'
+    else:
+        return 'Python'
+
+
+def _select_scheme(ob, name):
+    scheme = _inject_headers(name, _load_scheme(_resolve_scheme(name)))
+    vars(ob).update(_remove_set(ob, _scheme_attrs(scheme)))
+
+
+def _remove_set(ob, attrs):
+    """
+    Include only attrs that are None in ob.
+    """
+    return {key: value for key, value in attrs.items() if getattr(ob, key) is None}
+
+
+def _resolve_scheme(name):
+    os_name, sep, key = name.partition('_')
+    try:
+        resolved = sysconfig.get_preferred_scheme(key)
+    except Exception:
+        resolved = fw.scheme(name)
+    return resolved
+
+
+def _load_scheme(name):
+    return _load_schemes()[name]
+
+
+def _inject_headers(name, scheme):
+    """
+    Given a scheme name and the resolved scheme,
+    if the scheme does not include headers, resolve
+    the fallback scheme for the name and use headers
+    from it. pypa/distutils#88
+    """
+    # Bypass the preferred scheme, which may not
+    # have defined headers.
+    fallback = _load_scheme(name)
+    scheme.setdefault('headers', fallback['headers'])
+    return scheme
+
+
+def _scheme_attrs(scheme):
+    """Resolve install directories by applying the install schemes."""
+    return {f'install_{key}': scheme[key] for key in SCHEME_KEYS}
+
+
+class install(Command):
+    description = "install everything from build directory"
+
+    user_options = [
+        # Select installation scheme and set base director(y|ies)
+        ('prefix=', None, "installation prefix"),
+        ('exec-prefix=', None, "(Unix only) prefix for platform-specific files"),
+        ('home=', None, "(Unix only) home directory to install under"),
+        # Or, just set the base director(y|ies)
+        (
+            'install-base=',
+            None,
+            "base installation directory (instead of --prefix or --home)",
+        ),
+        (
+            'install-platbase=',
+            None,
+            "base installation directory for platform-specific files (instead of --exec-prefix or --home)",
+        ),
+        ('root=', None, "install everything relative to this alternate root directory"),
+        # Or, explicitly set the installation scheme
+        (
+            'install-purelib=',
+            None,
+            "installation directory for pure Python module distributions",
+        ),
+        (
+            'install-platlib=',
+            None,
+            "installation directory for non-pure module distributions",
+        ),
+        (
+            'install-lib=',
+            None,
+            "installation directory for all module distributions (overrides --install-purelib and --install-platlib)",
+        ),
+        ('install-headers=', None, "installation directory for C/C++ headers"),
+        ('install-scripts=', None, "installation directory for Python scripts"),
+        ('install-data=', None, "installation directory for data files"),
+        # Byte-compilation options -- see install_lib.py for details, as
+        # these are duplicated from there (but only install_lib does
+        # anything with them).
+        ('compile', 'c', "compile .py to .pyc [default]"),
+        ('no-compile', None, "don't compile .py files"),
+        (
+            'optimize=',
+            'O',
+            "also compile with optimization: -O1 for \"python -O\", "
+            "-O2 for \"python -OO\", and -O0 to disable [default: -O0]",
+        ),
+        # Miscellaneous control options
+        ('force', 'f', "force installation (overwrite any existing files)"),
+        ('skip-build', None, "skip rebuilding everything (for testing/debugging)"),
+        # Where to install documentation (eventually!)
+        # ('doc-format=', None, "format of documentation to generate"),
+        # ('install-man=', None, "directory for Unix man pages"),
+        # ('install-html=', None, "directory for HTML documentation"),
+        # ('install-info=', None, "directory for GNU info files"),
+        ('record=', None, "filename in which to record list of installed files"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['compile', 'force', 'skip-build']
+
+    if HAS_USER_SITE:
+        user_options.append((
+            'user',
+            None,
+            f"install in user site-package '{USER_SITE}'",
+        ))
+        boolean_options.append('user')
+
+    negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}
+
+    def initialize_options(self) -> None:
+        """Initializes options."""
+        # High-level options: these select both an installation base
+        # and scheme.
+        self.prefix: str | None = None
+        self.exec_prefix: str | None = None
+        self.home: str | None = None
+        self.user = False
+
+        # These select only the installation base; it's up to the user to
+        # specify the installation scheme (currently, that means supplying
+        # the --install-{platlib,purelib,scripts,data} options).
+        self.install_base = None
+        self.install_platbase = None
+        self.root: str | None = None
+
+        # These options are the actual installation directories; if not
+        # supplied by the user, they are filled in using the installation
+        # scheme implied by prefix/exec-prefix/home and the contents of
+        # that installation scheme.
+        self.install_purelib = None  # for pure module distributions
+        self.install_platlib = None  # non-pure (dists w/ extensions)
+        self.install_headers = None  # for C/C++ headers
+        self.install_lib: str | None = None  # set to either purelib or platlib
+        self.install_scripts = None
+        self.install_data = None
+        self.install_userbase = USER_BASE
+        self.install_usersite = USER_SITE
+
+        self.compile = None
+        self.optimize = None
+
+        # Deprecated
+        # These two are for putting non-packagized distributions into their
+        # own directory and creating a .pth file if it makes sense.
+        # 'extra_path' comes from the setup file; 'install_path_file' can
+        # be turned off if it makes no sense to install a .pth file.  (But
+        # better to install it uselessly than to guess wrong and not
+        # install it when it's necessary and would be used!)  Currently,
+        # 'install_path_file' is always true unless some outsider meddles
+        # with it.
+        self.extra_path = None
+        self.install_path_file = True
+
+        # 'force' forces installation, even if target files are not
+        # out-of-date.  'skip_build' skips running the "build" command,
+        # handy if you know it's not necessary.  'warn_dir' (which is *not*
+        # a user option, it's just there so the bdist_* commands can turn
+        # it off) determines whether we warn about installing to a
+        # directory not in sys.path.
+        self.force = False
+        self.skip_build = False
+        self.warn_dir = True
+
+        # These are only here as a conduit from the 'build' command to the
+        # 'install_*' commands that do the real work.  ('build_base' isn't
+        # actually used anywhere, but it might be useful in future.)  They
+        # are not user options, because if the user told the install
+        # command where the build directory is, that wouldn't affect the
+        # build command.
+        self.build_base = None
+        self.build_lib = None
+
+        # Not defined yet because we don't know anything about
+        # documentation yet.
+        # self.install_man = None
+        # self.install_html = None
+        # self.install_info = None
+
+        self.record = None
+
+    # -- Option finalizing methods -------------------------------------
+    # (This is rather more involved than for most commands,
+    # because this is where the policy for installing third-
+    # party Python modules on various platforms given a wide
+    # array of user input is decided.  Yes, it's quite complex!)
+
+    def finalize_options(self) -> None:  # noqa: C901
+        """Finalizes options."""
+        # This method (and its helpers, like 'finalize_unix()',
+        # 'finalize_other()', and 'select_scheme()') is where the default
+        # installation directories for modules, extension modules, and
+        # anything else we care to install from a Python module
+        # distribution.  Thus, this code makes a pretty important policy
+        # statement about how third-party stuff is added to a Python
+        # installation!  Note that the actual work of installation is done
+        # by the relatively simple 'install_*' commands; they just take
+        # their orders from the installation directory options determined
+        # here.
+
+        # Check for errors/inconsistencies in the options; first, stuff
+        # that's wrong on any platform.
+
+        if (self.prefix or self.exec_prefix or self.home) and (
+            self.install_base or self.install_platbase
+        ):
+            raise DistutilsOptionError(
+                "must supply either prefix/exec-prefix/home or install-base/install-platbase -- not both"
+            )
+
+        if self.home and (self.prefix or self.exec_prefix):
+            raise DistutilsOptionError(
+                "must supply either home or prefix/exec-prefix -- not both"
+            )
+
+        if self.user and (
+            self.prefix
+            or self.exec_prefix
+            or self.home
+            or self.install_base
+            or self.install_platbase
+        ):
+            raise DistutilsOptionError(
+                "can't combine user with prefix, "
+                "exec_prefix/home, or install_(plat)base"
+            )
+
+        # Next, stuff that's wrong (or dubious) only on certain platforms.
+        if os.name != "posix":
+            if self.exec_prefix:
+                self.warn("exec-prefix option ignored on this platform")
+                self.exec_prefix = None
+
+        # Now the interesting logic -- so interesting that we farm it out
+        # to other methods.  The goal of these methods is to set the final
+        # values for the install_{lib,scripts,data,...}  options, using as
+        # input a heady brew of prefix, exec_prefix, home, install_base,
+        # install_platbase, user-supplied versions of
+        # install_{purelib,platlib,lib,scripts,data,...}, and the
+        # install schemes.  Phew!
+
+        self.dump_dirs("pre-finalize_{unix,other}")
+
+        if os.name == 'posix':
+            self.finalize_unix()
+        else:
+            self.finalize_other()
+
+        self.dump_dirs("post-finalize_{unix,other}()")
+
+        # Expand configuration variables, tilde, etc. in self.install_base
+        # and self.install_platbase -- that way, we can use $base or
+        # $platbase in the other installation directories and not worry
+        # about needing recursive variable expansion (shudder).
+
+        py_version = sys.version.split()[0]
+        (prefix, exec_prefix) = get_config_vars('prefix', 'exec_prefix')
+        try:
+            abiflags = sys.abiflags
+        except AttributeError:
+            # sys.abiflags may not be defined on all platforms.
+            abiflags = ''
+        local_vars = {
+            'dist_name': self.distribution.get_name(),
+            'dist_version': self.distribution.get_version(),
+            'dist_fullname': self.distribution.get_fullname(),
+            'py_version': py_version,
+            'py_version_short': f'{sys.version_info.major}.{sys.version_info.minor}',
+            'py_version_nodot': f'{sys.version_info.major}{sys.version_info.minor}',
+            'sys_prefix': prefix,
+            'prefix': prefix,
+            'sys_exec_prefix': exec_prefix,
+            'exec_prefix': exec_prefix,
+            'abiflags': abiflags,
+            'platlibdir': getattr(sys, 'platlibdir', 'lib'),
+            'implementation_lower': _get_implementation().lower(),
+            'implementation': _get_implementation(),
+        }
+
+        # vars for compatibility on older Pythons
+        compat_vars = dict(
+            # Python 3.9 and earlier
+            py_version_nodot_plat=getattr(sys, 'winver', '').replace('.', ''),
+        )
+
+        if HAS_USER_SITE:
+            local_vars['userbase'] = self.install_userbase
+            local_vars['usersite'] = self.install_usersite
+
+        self.config_vars = collections.ChainMap(
+            local_vars,
+            sysconfig.get_config_vars(),
+            compat_vars,
+            fw.vars(),
+        )
+
+        self.expand_basedirs()
+
+        self.dump_dirs("post-expand_basedirs()")
+
+        # Now define config vars for the base directories so we can expand
+        # everything else.
+        local_vars['base'] = self.install_base
+        local_vars['platbase'] = self.install_platbase
+
+        if DEBUG:
+            from pprint import pprint
+
+            print("config vars:")
+            pprint(dict(self.config_vars))
+
+        # Expand "~" and configuration variables in the installation
+        # directories.
+        self.expand_dirs()
+
+        self.dump_dirs("post-expand_dirs()")
+
+        # Create directories in the home dir:
+        if self.user:
+            self.create_home_path()
+
+        # Pick the actual directory to install all modules to: either
+        # install_purelib or install_platlib, depending on whether this
+        # module distribution is pure or not.  Of course, if the user
+        # already specified install_lib, use their selection.
+        if self.install_lib is None:
+            if self.distribution.has_ext_modules():  # has extensions: non-pure
+                self.install_lib = self.install_platlib
+            else:
+                self.install_lib = self.install_purelib
+
+        # Convert directories from Unix /-separated syntax to the local
+        # convention.
+        self.convert_paths(
+            'lib',
+            'purelib',
+            'platlib',
+            'scripts',
+            'data',
+            'headers',
+            'userbase',
+            'usersite',
+        )
+
+        # Deprecated
+        # Well, we're not actually fully completely finalized yet: we still
+        # have to deal with 'extra_path', which is the hack for allowing
+        # non-packagized module distributions (hello, Numerical Python!) to
+        # get their own directories.
+        self.handle_extra_path()
+        self.install_libbase = self.install_lib  # needed for .pth file
+        self.install_lib = os.path.join(self.install_lib, self.extra_dirs)
+
+        # If a new root directory was supplied, make all the installation
+        # dirs relative to it.
+        if self.root is not None:
+            self.change_roots(
+                'libbase', 'lib', 'purelib', 'platlib', 'scripts', 'data', 'headers'
+            )
+
+        self.dump_dirs("after prepending root")
+
+        # Find out the build directories, ie. where to install from.
+        self.set_undefined_options(
+            'build', ('build_base', 'build_base'), ('build_lib', 'build_lib')
+        )
+
+        # Punt on doc directories for now -- after all, we're punting on
+        # documentation completely!
+
+    def dump_dirs(self, msg) -> None:
+        """Dumps the list of user options."""
+        if not DEBUG:
+            return
+        from ..fancy_getopt import longopt_xlate
+
+        log.debug(msg + ":")
+        for opt in self.user_options:
+            opt_name = opt[0]
+            if opt_name[-1] == "=":
+                opt_name = opt_name[0:-1]
+            if opt_name in self.negative_opt:
+                opt_name = self.negative_opt[opt_name]
+                opt_name = opt_name.translate(longopt_xlate)
+                val = not getattr(self, opt_name)
+            else:
+                opt_name = opt_name.translate(longopt_xlate)
+                val = getattr(self, opt_name)
+            log.debug("  %s: %s", opt_name, val)
+
+    def finalize_unix(self) -> None:
+        """Finalizes options for posix platforms."""
+        if self.install_base is not None or self.install_platbase is not None:
+            incomplete_scheme = (
+                (
+                    self.install_lib is None
+                    and self.install_purelib is None
+                    and self.install_platlib is None
+                )
+                or self.install_headers is None
+                or self.install_scripts is None
+                or self.install_data is None
+            )
+            if incomplete_scheme:
+                raise DistutilsOptionError(
+                    "install-base or install-platbase supplied, but "
+                    "installation scheme is incomplete"
+                )
+            return
+
+        if self.user:
+            if self.install_userbase is None:
+                raise DistutilsPlatformError("User base directory is not specified")
+            self.install_base = self.install_platbase = self.install_userbase
+            self.select_scheme("posix_user")
+        elif self.home is not None:
+            self.install_base = self.install_platbase = self.home
+            self.select_scheme("posix_home")
+        else:
+            if self.prefix is None:
+                if self.exec_prefix is not None:
+                    raise DistutilsOptionError(
+                        "must not supply exec-prefix without prefix"
+                    )
+
+                # Allow Fedora to add components to the prefix
+                _prefix_addition = getattr(sysconfig, '_prefix_addition', "")
+
+                self.prefix = os.path.normpath(sys.prefix) + _prefix_addition
+                self.exec_prefix = os.path.normpath(sys.exec_prefix) + _prefix_addition
+
+            else:
+                if self.exec_prefix is None:
+                    self.exec_prefix = self.prefix
+
+            self.install_base = self.prefix
+            self.install_platbase = self.exec_prefix
+            self.select_scheme("posix_prefix")
+
+    def finalize_other(self) -> None:
+        """Finalizes options for non-posix platforms"""
+        if self.user:
+            if self.install_userbase is None:
+                raise DistutilsPlatformError("User base directory is not specified")
+            self.install_base = self.install_platbase = self.install_userbase
+            self.select_scheme(os.name + "_user")
+        elif self.home is not None:
+            self.install_base = self.install_platbase = self.home
+            self.select_scheme("posix_home")
+        else:
+            if self.prefix is None:
+                self.prefix = os.path.normpath(sys.prefix)
+
+            self.install_base = self.install_platbase = self.prefix
+            try:
+                self.select_scheme(os.name)
+            except KeyError:
+                raise DistutilsPlatformError(
+                    f"I don't know how to install stuff on '{os.name}'"
+                )
+
+    def select_scheme(self, name) -> None:
+        _select_scheme(self, name)
+
+    def _expand_attrs(self, attrs):
+        for attr in attrs:
+            val = getattr(self, attr)
+            if val is not None:
+                if os.name in ('posix', 'nt'):
+                    val = os.path.expanduser(val)
+                val = subst_vars(val, self.config_vars)
+                setattr(self, attr, val)
+
+    def expand_basedirs(self) -> None:
+        """Calls `os.path.expanduser` on install_base, install_platbase and
+        root."""
+        self._expand_attrs(['install_base', 'install_platbase', 'root'])
+
+    def expand_dirs(self) -> None:
+        """Calls `os.path.expanduser` on install dirs."""
+        self._expand_attrs([
+            'install_purelib',
+            'install_platlib',
+            'install_lib',
+            'install_headers',
+            'install_scripts',
+            'install_data',
+        ])
+
+    def convert_paths(self, *names) -> None:
+        """Call `convert_path` over `names`."""
+        for name in names:
+            attr = "install_" + name
+            setattr(self, attr, convert_path(getattr(self, attr)))
+
+    def handle_extra_path(self) -> None:
+        """Set `path_file` and `extra_dirs` using `extra_path`."""
+        if self.extra_path is None:
+            self.extra_path = self.distribution.extra_path
+
+        if self.extra_path is not None:
+            log.warning(
+                "Distribution option extra_path is deprecated. "
+                "See issue27919 for details."
+            )
+            if isinstance(self.extra_path, str):
+                self.extra_path = self.extra_path.split(',')
+
+            if len(self.extra_path) == 1:
+                path_file = extra_dirs = self.extra_path[0]
+            elif len(self.extra_path) == 2:
+                path_file, extra_dirs = self.extra_path
+            else:
+                raise DistutilsOptionError(
+                    "'extra_path' option must be a list, tuple, or "
+                    "comma-separated string with 1 or 2 elements"
+                )
+
+            # convert to local form in case Unix notation used (as it
+            # should be in setup scripts)
+            extra_dirs = convert_path(extra_dirs)
+        else:
+            path_file = None
+            extra_dirs = ''
+
+        # XXX should we warn if path_file and not extra_dirs? (in which
+        # case the path file would be harmless but pointless)
+        self.path_file = path_file
+        self.extra_dirs = extra_dirs
+
+    def change_roots(self, *names) -> None:
+        """Change the install directories pointed by name using root."""
+        for name in names:
+            attr = "install_" + name
+            setattr(self, attr, change_root(self.root, getattr(self, attr)))
+
+    def create_home_path(self) -> None:
+        """Create directories under ~."""
+        if not self.user:
+            return
+        home = convert_path(os.path.expanduser("~"))
+        for path in self.config_vars.values():
+            if str(path).startswith(home) and not os.path.isdir(path):
+                self.debug_print(f"os.makedirs('{path}', 0o700)")
+                os.makedirs(path, 0o700)
+
+    # -- Command execution methods -------------------------------------
+
+    def run(self):
+        """Runs the command."""
+        # Obviously have to build before we can install
+        if not self.skip_build:
+            self.run_command('build')
+            # If we built for any other platform, we can't install.
+            build_plat = self.distribution.get_command_obj('build').plat_name
+            # check warn_dir - it is a clue that the 'install' is happening
+            # internally, and not to sys.path, so we don't check the platform
+            # matches what we are running.
+            if self.warn_dir and build_plat != get_platform():
+                raise DistutilsPlatformError("Can't install when cross-compiling")
+
+        # Run all sub-commands (at least those that need to be run)
+        for cmd_name in self.get_sub_commands():
+            self.run_command(cmd_name)
+
+        if self.path_file:
+            self.create_path_file()
+
+        # write list of installed files, if requested.
+        if self.record:
+            outputs = self.get_outputs()
+            if self.root:  # strip any package prefix
+                root_len = len(self.root)
+                for counter in range(len(outputs)):
+                    outputs[counter] = outputs[counter][root_len:]
+            self.execute(
+                write_file,
+                (self.record, outputs),
+                f"writing list of installed files to '{self.record}'",
+            )
+
+        sys_path = map(os.path.normpath, sys.path)
+        sys_path = map(os.path.normcase, sys_path)
+        install_lib = os.path.normcase(os.path.normpath(self.install_lib))
+        if (
+            self.warn_dir
+            and not (self.path_file and self.install_path_file)
+            and install_lib not in sys_path
+        ):
+            log.debug(
+                (
+                    "modules installed to '%s', which is not in "
+                    "Python's module search path (sys.path) -- "
+                    "you'll have to change the search path yourself"
+                ),
+                self.install_lib,
+            )
+
+    def create_path_file(self):
+        """Creates the .pth file"""
+        filename = os.path.join(self.install_libbase, self.path_file + ".pth")
+        if self.install_path_file:
+            self.execute(
+                write_file, (filename, [self.extra_dirs]), f"creating {filename}"
+            )
+        else:
+            self.warn(f"path file '{filename}' not created")
+
+    # -- Reporting methods ---------------------------------------------
+
+    def get_outputs(self):
+        """Assembles the outputs of all the sub-commands."""
+        outputs = []
+        for cmd_name in self.get_sub_commands():
+            cmd = self.get_finalized_command(cmd_name)
+            # Add the contents of cmd.get_outputs(), ensuring
+            # that outputs doesn't contain duplicate entries
+            for filename in cmd.get_outputs():
+                if filename not in outputs:
+                    outputs.append(filename)
+
+        if self.path_file and self.install_path_file:
+            outputs.append(os.path.join(self.install_libbase, self.path_file + ".pth"))
+
+        return outputs
+
+    def get_inputs(self):
+        """Returns the inputs of all the sub-commands"""
+        # XXX gee, this looks familiar ;-(
+        inputs = []
+        for cmd_name in self.get_sub_commands():
+            cmd = self.get_finalized_command(cmd_name)
+            inputs.extend(cmd.get_inputs())
+
+        return inputs
+
+    # -- Predicates for sub-command list -------------------------------
+
+    def has_lib(self):
+        """Returns true if the current distribution has any Python
+        modules to install."""
+        return (
+            self.distribution.has_pure_modules() or self.distribution.has_ext_modules()
+        )
+
+    def has_headers(self):
+        """Returns true if the current distribution has any headers to
+        install."""
+        return self.distribution.has_headers()
+
+    def has_scripts(self):
+        """Returns true if the current distribution has any scripts to.
+        install."""
+        return self.distribution.has_scripts()
+
+    def has_data(self):
+        """Returns true if the current distribution has any data to.
+        install."""
+        return self.distribution.has_data_files()
+
+    # 'sub_commands': a list of commands this command might have to run to
+    # get its work done.  See cmd.py for more info.
+    sub_commands = [
+        ('install_lib', has_lib),
+        ('install_headers', has_headers),
+        ('install_scripts', has_scripts),
+        ('install_data', has_data),
+        ('install_egg_info', lambda self: True),
+    ]
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_data.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_data.py
new file mode 100644
index 00000000..4ad186e8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_data.py
@@ -0,0 +1,94 @@
+"""distutils.command.install_data
+
+Implements the Distutils 'install_data' command, for installing
+platform-independent data files."""
+
+# contributed by Bastian Kleineidam
+
+from __future__ import annotations
+
+import functools
+import os
+from collections.abc import Iterable
+from typing import ClassVar
+
+from ..core import Command
+from ..util import change_root, convert_path
+
+
+class install_data(Command):
+    description = "install data files"
+
+    user_options = [
+        (
+            'install-dir=',
+            'd',
+            "base directory for installing data files [default: installation base dir]",
+        ),
+        ('root=', None, "install everything relative to this alternate root directory"),
+        ('force', 'f', "force installation (overwrite existing files)"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['force']
+
+    def initialize_options(self):
+        self.install_dir = None
+        self.outfiles = []
+        self.root = None
+        self.force = False
+        self.data_files = self.distribution.data_files
+        self.warn_dir = True
+
+    def finalize_options(self) -> None:
+        self.set_undefined_options(
+            'install',
+            ('install_data', 'install_dir'),
+            ('root', 'root'),
+            ('force', 'force'),
+        )
+
+    def run(self) -> None:
+        self.mkpath(self.install_dir)
+        for f in self.data_files:
+            self._copy(f)
+
+    @functools.singledispatchmethod
+    def _copy(self, f: tuple[str | os.PathLike, Iterable[str | os.PathLike]]):
+        # it's a tuple with path to install to and a list of files
+        dir = convert_path(f[0])
+        if not os.path.isabs(dir):
+            dir = os.path.join(self.install_dir, dir)
+        elif self.root:
+            dir = change_root(self.root, dir)
+        self.mkpath(dir)
+
+        if f[1] == []:
+            # If there are no files listed, the user must be
+            # trying to create an empty directory, so add the
+            # directory to the list of output files.
+            self.outfiles.append(dir)
+        else:
+            # Copy files, adding them to the list of output files.
+            for data in f[1]:
+                data = convert_path(data)
+                (out, _) = self.copy_file(data, dir)
+                self.outfiles.append(out)
+
+    @_copy.register(str)
+    @_copy.register(os.PathLike)
+    def _(self, f: str | os.PathLike):
+        # it's a simple file, so copy it
+        f = convert_path(f)
+        if self.warn_dir:
+            self.warn(
+                "setup script did not provide a directory for "
+                f"'{f}' -- installing right in '{self.install_dir}'"
+            )
+        (out, _) = self.copy_file(f, self.install_dir)
+        self.outfiles.append(out)
+
+    def get_inputs(self):
+        return self.data_files or []
+
+    def get_outputs(self):
+        return self.outfiles
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_egg_info.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_egg_info.py
new file mode 100644
index 00000000..230e94ab
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_egg_info.py
@@ -0,0 +1,91 @@
+"""
+distutils.command.install_egg_info
+
+Implements the Distutils 'install_egg_info' command, for installing
+a package's PKG-INFO metadata.
+"""
+
+import os
+import re
+import sys
+from typing import ClassVar
+
+from .. import dir_util
+from .._log import log
+from ..cmd import Command
+
+
+class install_egg_info(Command):
+    """Install an .egg-info file for the package"""
+
+    description = "Install package's PKG-INFO metadata as an .egg-info file"
+    user_options: ClassVar[list[tuple[str, str, str]]] = [
+        ('install-dir=', 'd', "directory to install to"),
+    ]
+
+    def initialize_options(self):
+        self.install_dir = None
+
+    @property
+    def basename(self):
+        """
+        Allow basename to be overridden by child class.
+        Ref pypa/distutils#2.
+        """
+        name = to_filename(safe_name(self.distribution.get_name()))
+        version = to_filename(safe_version(self.distribution.get_version()))
+        return f"{name}-{version}-py{sys.version_info.major}.{sys.version_info.minor}.egg-info"
+
+    def finalize_options(self):
+        self.set_undefined_options('install_lib', ('install_dir', 'install_dir'))
+        self.target = os.path.join(self.install_dir, self.basename)
+        self.outputs = [self.target]
+
+    def run(self):
+        target = self.target
+        if os.path.isdir(target) and not os.path.islink(target):
+            dir_util.remove_tree(target, dry_run=self.dry_run)
+        elif os.path.exists(target):
+            self.execute(os.unlink, (self.target,), "Removing " + target)
+        elif not os.path.isdir(self.install_dir):
+            self.execute(
+                os.makedirs, (self.install_dir,), "Creating " + self.install_dir
+            )
+        log.info("Writing %s", target)
+        if not self.dry_run:
+            with open(target, 'w', encoding='UTF-8') as f:
+                self.distribution.metadata.write_pkg_file(f)
+
+    def get_outputs(self):
+        return self.outputs
+
+
+# The following routines are taken from setuptools' pkg_resources module and
+# can be replaced by importing them from pkg_resources once it is included
+# in the stdlib.
+
+
+def safe_name(name):
+    """Convert an arbitrary string to a standard distribution name
+
+    Any runs of non-alphanumeric/. characters are replaced with a single '-'.
+    """
+    return re.sub('[^A-Za-z0-9.]+', '-', name)
+
+
+def safe_version(version):
+    """Convert an arbitrary string to a standard version string
+
+    Spaces become dots, and all other non-alphanumeric characters become
+    dashes, with runs of multiple dashes condensed to a single dash.
+    """
+    version = version.replace(' ', '.')
+    return re.sub('[^A-Za-z0-9.]+', '-', version)
+
+
+def to_filename(name):
+    """Convert a project or version name to its filename-escaped form
+
+    Any '-' characters are currently replaced with '_'.
+    """
+    return name.replace('-', '_')
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_headers.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_headers.py
new file mode 100644
index 00000000..97af1371
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_headers.py
@@ -0,0 +1,46 @@
+"""distutils.command.install_headers
+
+Implements the Distutils 'install_headers' command, to install C/C++ header
+files to the Python include directory."""
+
+from typing import ClassVar
+
+from ..core import Command
+
+
+# XXX force is never used
+class install_headers(Command):
+    description = "install C/C++ header files"
+
+    user_options: ClassVar[list[tuple[str, str, str]]] = [
+        ('install-dir=', 'd', "directory to install header files to"),
+        ('force', 'f', "force installation (overwrite existing files)"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['force']
+
+    def initialize_options(self):
+        self.install_dir = None
+        self.force = False
+        self.outfiles = []
+
+    def finalize_options(self):
+        self.set_undefined_options(
+            'install', ('install_headers', 'install_dir'), ('force', 'force')
+        )
+
+    def run(self):
+        headers = self.distribution.headers
+        if not headers:
+            return
+
+        self.mkpath(self.install_dir)
+        for header in headers:
+            (out, _) = self.copy_file(header, self.install_dir)
+            self.outfiles.append(out)
+
+    def get_inputs(self):
+        return self.distribution.headers or []
+
+    def get_outputs(self):
+        return self.outfiles
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_lib.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_lib.py
new file mode 100644
index 00000000..2aababf8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_lib.py
@@ -0,0 +1,238 @@
+"""distutils.command.install_lib
+
+Implements the Distutils 'install_lib' command
+(install all Python modules)."""
+
+from __future__ import annotations
+
+import importlib.util
+import os
+import sys
+from typing import Any, ClassVar
+
+from ..core import Command
+from ..errors import DistutilsOptionError
+
+# Extension for Python source files.
+PYTHON_SOURCE_EXTENSION = ".py"
+
+
+class install_lib(Command):
+    description = "install all Python modules (extensions and pure Python)"
+
+    # The byte-compilation options are a tad confusing.  Here are the
+    # possible scenarios:
+    #   1) no compilation at all (--no-compile --no-optimize)
+    #   2) compile .pyc only (--compile --no-optimize; default)
+    #   3) compile .pyc and "opt-1" .pyc (--compile --optimize)
+    #   4) compile "opt-1" .pyc only (--no-compile --optimize)
+    #   5) compile .pyc and "opt-2" .pyc (--compile --optimize-more)
+    #   6) compile "opt-2" .pyc only (--no-compile --optimize-more)
+    #
+    # The UI for this is two options, 'compile' and 'optimize'.
+    # 'compile' is strictly boolean, and only decides whether to
+    # generate .pyc files.  'optimize' is three-way (0, 1, or 2), and
+    # decides both whether to generate .pyc files and what level of
+    # optimization to use.
+
+    user_options = [
+        ('install-dir=', 'd', "directory to install to"),
+        ('build-dir=', 'b', "build directory (where to install from)"),
+        ('force', 'f', "force installation (overwrite existing files)"),
+        ('compile', 'c', "compile .py to .pyc [default]"),
+        ('no-compile', None, "don't compile .py files"),
+        (
+            'optimize=',
+            'O',
+            "also compile with optimization: -O1 for \"python -O\", "
+            "-O2 for \"python -OO\", and -O0 to disable [default: -O0]",
+        ),
+        ('skip-build', None, "skip the build steps"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['force', 'compile', 'skip-build']
+    negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}
+
+    def initialize_options(self):
+        # let the 'install' command dictate our installation directory
+        self.install_dir = None
+        self.build_dir = None
+        self.force = False
+        self.compile = None
+        self.optimize = None
+        self.skip_build = None
+
+    def finalize_options(self) -> None:
+        # Get all the information we need to install pure Python modules
+        # from the umbrella 'install' command -- build (source) directory,
+        # install (target) directory, and whether to compile .py files.
+        self.set_undefined_options(
+            'install',
+            ('build_lib', 'build_dir'),
+            ('install_lib', 'install_dir'),
+            ('force', 'force'),
+            ('compile', 'compile'),
+            ('optimize', 'optimize'),
+            ('skip_build', 'skip_build'),
+        )
+
+        if self.compile is None:
+            self.compile = True
+        if self.optimize is None:
+            self.optimize = False
+
+        if not isinstance(self.optimize, int):
+            try:
+                self.optimize = int(self.optimize)
+            except ValueError:
+                pass
+            if self.optimize not in (0, 1, 2):
+                raise DistutilsOptionError("optimize must be 0, 1, or 2")
+
+    def run(self) -> None:
+        # Make sure we have built everything we need first
+        self.build()
+
+        # Install everything: simply dump the entire contents of the build
+        # directory to the installation directory (that's the beauty of
+        # having a build directory!)
+        outfiles = self.install()
+
+        # (Optionally) compile .py to .pyc
+        if outfiles is not None and self.distribution.has_pure_modules():
+            self.byte_compile(outfiles)
+
+    # -- Top-level worker functions ------------------------------------
+    # (called from 'run()')
+
+    def build(self) -> None:
+        if not self.skip_build:
+            if self.distribution.has_pure_modules():
+                self.run_command('build_py')
+            if self.distribution.has_ext_modules():
+                self.run_command('build_ext')
+
+    # Any: https://typing.readthedocs.io/en/latest/guides/writing_stubs.html#the-any-trick
+    def install(self) -> list[str] | Any:
+        if os.path.isdir(self.build_dir):
+            outfiles = self.copy_tree(self.build_dir, self.install_dir)
+        else:
+            self.warn(
+                f"'{self.build_dir}' does not exist -- no Python modules to install"
+            )
+            return
+        return outfiles
+
+    def byte_compile(self, files) -> None:
+        if sys.dont_write_bytecode:
+            self.warn('byte-compiling is disabled, skipping.')
+            return
+
+        from ..util import byte_compile
+
+        # Get the "--root" directory supplied to the "install" command,
+        # and use it as a prefix to strip off the purported filename
+        # encoded in bytecode files.  This is far from complete, but it
+        # should at least generate usable bytecode in RPM distributions.
+        install_root = self.get_finalized_command('install').root
+
+        if self.compile:
+            byte_compile(
+                files,
+                optimize=0,
+                force=self.force,
+                prefix=install_root,
+                dry_run=self.dry_run,
+            )
+        if self.optimize > 0:
+            byte_compile(
+                files,
+                optimize=self.optimize,
+                force=self.force,
+                prefix=install_root,
+                verbose=self.verbose,
+                dry_run=self.dry_run,
+            )
+
+    # -- Utility methods -----------------------------------------------
+
+    def _mutate_outputs(self, has_any, build_cmd, cmd_option, output_dir):
+        if not has_any:
+            return []
+
+        build_cmd = self.get_finalized_command(build_cmd)
+        build_files = build_cmd.get_outputs()
+        build_dir = getattr(build_cmd, cmd_option)
+
+        prefix_len = len(build_dir) + len(os.sep)
+        outputs = [os.path.join(output_dir, file[prefix_len:]) for file in build_files]
+
+        return outputs
+
+    def _bytecode_filenames(self, py_filenames):
+        bytecode_files = []
+        for py_file in py_filenames:
+            # Since build_py handles package data installation, the
+            # list of outputs can contain more than just .py files.
+            # Make sure we only report bytecode for the .py files.
+            ext = os.path.splitext(os.path.normcase(py_file))[1]
+            if ext != PYTHON_SOURCE_EXTENSION:
+                continue
+            if self.compile:
+                bytecode_files.append(
+                    importlib.util.cache_from_source(py_file, optimization='')
+                )
+            if self.optimize > 0:
+                bytecode_files.append(
+                    importlib.util.cache_from_source(
+                        py_file, optimization=self.optimize
+                    )
+                )
+
+        return bytecode_files
+
+    # -- External interface --------------------------------------------
+    # (called by outsiders)
+
+    def get_outputs(self):
+        """Return the list of files that would be installed if this command
+        were actually run.  Not affected by the "dry-run" flag or whether
+        modules have actually been built yet.
+        """
+        pure_outputs = self._mutate_outputs(
+            self.distribution.has_pure_modules(),
+            'build_py',
+            'build_lib',
+            self.install_dir,
+        )
+        if self.compile:
+            bytecode_outputs = self._bytecode_filenames(pure_outputs)
+        else:
+            bytecode_outputs = []
+
+        ext_outputs = self._mutate_outputs(
+            self.distribution.has_ext_modules(),
+            'build_ext',
+            'build_lib',
+            self.install_dir,
+        )
+
+        return pure_outputs + bytecode_outputs + ext_outputs
+
+    def get_inputs(self):
+        """Get the list of files that are input to this command, ie. the
+        files that get installed as they are named in the build tree.
+        The files in this list correspond one-to-one to the output
+        filenames returned by 'get_outputs()'.
+        """
+        inputs = []
+
+        if self.distribution.has_pure_modules():
+            build_py = self.get_finalized_command('build_py')
+            inputs.extend(build_py.get_outputs())
+
+        if self.distribution.has_ext_modules():
+            build_ext = self.get_finalized_command('build_ext')
+            inputs.extend(build_ext.get_outputs())
+
+        return inputs
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_scripts.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_scripts.py
new file mode 100644
index 00000000..92e86941
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/install_scripts.py
@@ -0,0 +1,62 @@
+"""distutils.command.install_scripts
+
+Implements the Distutils 'install_scripts' command, for installing
+Python scripts."""
+
+# contributed by Bastian Kleineidam
+
+import os
+from distutils._log import log
+from stat import ST_MODE
+from typing import ClassVar
+
+from ..core import Command
+
+
+class install_scripts(Command):
+    description = "install scripts (Python or otherwise)"
+
+    user_options = [
+        ('install-dir=', 'd', "directory to install scripts to"),
+        ('build-dir=', 'b', "build directory (where to install from)"),
+        ('force', 'f', "force installation (overwrite existing files)"),
+        ('skip-build', None, "skip the build steps"),
+    ]
+
+    boolean_options: ClassVar[list[str]] = ['force', 'skip-build']
+
+    def initialize_options(self):
+        self.install_dir = None
+        self.force = False
+        self.build_dir = None
+        self.skip_build = None
+
+    def finalize_options(self) -> None:
+        self.set_undefined_options('build', ('build_scripts', 'build_dir'))
+        self.set_undefined_options(
+            'install',
+            ('install_scripts', 'install_dir'),
+            ('force', 'force'),
+            ('skip_build', 'skip_build'),
+        )
+
+    def run(self) -> None:
+        if not self.skip_build:
+            self.run_command('build_scripts')
+        self.outfiles = self.copy_tree(self.build_dir, self.install_dir)
+        if os.name == 'posix':
+            # Set the executable bits (owner, group, and world) on
+            # all the scripts we just installed.
+            for file in self.get_outputs():
+                if self.dry_run:
+                    log.info("changing mode of %s", file)
+                else:
+                    mode = ((os.stat(file)[ST_MODE]) | 0o555) & 0o7777
+                    log.info("changing mode of %s to %o", file, mode)
+                    os.chmod(file, mode)
+
+    def get_inputs(self):
+        return self.distribution.scripts or []
+
+    def get_outputs(self):
+        return self.outfiles or []
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/sdist.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/sdist.py
new file mode 100644
index 00000000..b3bf0c32
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/command/sdist.py
@@ -0,0 +1,521 @@
+"""distutils.command.sdist
+
+Implements the Distutils 'sdist' command (create a source distribution)."""
+
+from __future__ import annotations
+
+import os
+import sys
+from collections.abc import Callable
+from distutils import archive_util, dir_util, file_util
+from distutils._log import log
+from glob import glob
+from itertools import filterfalse
+from typing import ClassVar
+
+from ..core import Command
+from ..errors import DistutilsOptionError, DistutilsTemplateError
+from ..filelist import FileList
+from ..text_file import TextFile
+from ..util import convert_path
+
+
+def show_formats():
+    """Print all possible values for the 'formats' option (used by
+    the "--help-formats" command-line option).
+    """
+    from ..archive_util import ARCHIVE_FORMATS
+    from ..fancy_getopt import FancyGetopt
+
+    formats = sorted(
+        ("formats=" + format, None, ARCHIVE_FORMATS[format][2])
+        for format in ARCHIVE_FORMATS.keys()
+    )
+    FancyGetopt(formats).print_help("List of available source distribution formats:")
+
+
+class sdist(Command):
+    description = "create a source distribution (tarball, zip file, etc.)"
+
+    def checking_metadata(self) -> bool:
+        """Callable used for the check sub-command.
+
+        Placed here so user_options can view it"""
+        return self.metadata_check
+
+    user_options = [
+        ('template=', 't', "name of manifest template file [default: MANIFEST.in]"),
+        ('manifest=', 'm', "name of manifest file [default: MANIFEST]"),
+        (
+            'use-defaults',
+            None,
+            "include the default file set in the manifest "
+            "[default; disable with --no-defaults]",
+        ),
+        ('no-defaults', None, "don't include the default file set"),
+        (
+            'prune',
+            None,
+            "specifically exclude files/directories that should not be "
+            "distributed (build tree, RCS/CVS dirs, etc.) "
+            "[default; disable with --no-prune]",
+        ),
+        ('no-prune', None, "don't automatically exclude anything"),
+        (
+            'manifest-only',
+            'o',
+            "just regenerate the manifest and then stop (implies --force-manifest)",
+        ),
+        (
+            'force-manifest',
+            'f',
+            "forcibly regenerate the manifest and carry on as usual. "
+            "Deprecated: now the manifest is always regenerated.",
+        ),
+        ('formats=', None, "formats for source distribution (comma-separated list)"),
+        (
+            'keep-temp',
+            'k',
+            "keep the distribution tree around after creating " + "archive file(s)",
+        ),
+        (
+            'dist-dir=',
+            'd',
+            "directory to put the source distribution archive(s) in [default: dist]",
+        ),
+        (
+            'metadata-check',
+            None,
+            "Ensure that all required elements of meta-data "
+            "are supplied. Warn if any missing. [default]",
+        ),
+        (
+            'owner=',
+            'u',
+            "Owner name used when creating a tar file [default: current user]",
+        ),
+        (
+            'group=',
+            'g',
+            "Group name used when creating a tar file [default: current group]",
+        ),
+    ]
+
+    boolean_options: ClassVar[list[str]] = [
+        'use-defaults',
+        'prune',
+        'manifest-only',
+        'force-manifest',
+        'keep-temp',
+        'metadata-check',
+    ]
+
+    help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
+        ('help-formats', None, "list available distribution formats", show_formats),
+    ]
+
+    negative_opt: ClassVar[dict[str, str]] = {
+        'no-defaults': 'use-defaults',
+        'no-prune': 'prune',
+    }
+
+    sub_commands = [('check', checking_metadata)]
+
+    READMES: ClassVar[tuple[str, ...]] = ('README', 'README.txt', 'README.rst')
+
+    def initialize_options(self):
+        # 'template' and 'manifest' are, respectively, the names of
+        # the manifest template and manifest file.
+        self.template = None
+        self.manifest = None
+
+        # 'use_defaults': if true, we will include the default file set
+        # in the manifest
+        self.use_defaults = True
+        self.prune = True
+
+        self.manifest_only = False
+        self.force_manifest = False
+
+        self.formats = ['gztar']
+        self.keep_temp = False
+        self.dist_dir = None
+
+        self.archive_files = None
+        self.metadata_check = True
+        self.owner = None
+        self.group = None
+
+    def finalize_options(self) -> None:
+        if self.manifest is None:
+            self.manifest = "MANIFEST"
+        if self.template is None:
+            self.template = "MANIFEST.in"
+
+        self.ensure_string_list('formats')
+
+        bad_format = archive_util.check_archive_formats(self.formats)
+        if bad_format:
+            raise DistutilsOptionError(f"unknown archive format '{bad_format}'")
+
+        if self.dist_dir is None:
+            self.dist_dir = "dist"
+
+    def run(self) -> None:
+        # 'filelist' contains the list of files that will make up the
+        # manifest
+        self.filelist = FileList()
+
+        # Run sub commands
+        for cmd_name in self.get_sub_commands():
+            self.run_command(cmd_name)
+
+        # Do whatever it takes to get the list of files to process
+        # (process the manifest template, read an existing manifest,
+        # whatever).  File list is accumulated in 'self.filelist'.
+        self.get_file_list()
+
+        # If user just wanted us to regenerate the manifest, stop now.
+        if self.manifest_only:
+            return
+
+        # Otherwise, go ahead and create the source distribution tarball,
+        # or zipfile, or whatever.
+        self.make_distribution()
+
+    def get_file_list(self) -> None:
+        """Figure out the list of files to include in the source
+        distribution, and put it in 'self.filelist'.  This might involve
+        reading the manifest template (and writing the manifest), or just
+        reading the manifest, or just using the default file set -- it all
+        depends on the user's options.
+        """
+        # new behavior when using a template:
+        # the file list is recalculated every time because
+        # even if MANIFEST.in or setup.py are not changed
+        # the user might have added some files in the tree that
+        # need to be included.
+        #
+        #  This makes --force the default and only behavior with templates.
+        template_exists = os.path.isfile(self.template)
+        if not template_exists and self._manifest_is_not_generated():
+            self.read_manifest()
+            self.filelist.sort()
+            self.filelist.remove_duplicates()
+            return
+
+        if not template_exists:
+            self.warn(
+                ("manifest template '%s' does not exist " + "(using default file list)")
+                % self.template
+            )
+        self.filelist.findall()
+
+        if self.use_defaults:
+            self.add_defaults()
+
+        if template_exists:
+            self.read_template()
+
+        if self.prune:
+            self.prune_file_list()
+
+        self.filelist.sort()
+        self.filelist.remove_duplicates()
+        self.write_manifest()
+
+    def add_defaults(self) -> None:
+        """Add all the default files to self.filelist:
+          - README or README.txt
+          - setup.py
+          - tests/test*.py and test/test*.py
+          - all pure Python modules mentioned in setup script
+          - all files pointed by package_data (build_py)
+          - all files defined in data_files.
+          - all files defined as scripts.
+          - all C sources listed as part of extensions or C libraries
+            in the setup script (doesn't catch C headers!)
+        Warns if (README or README.txt) or setup.py are missing; everything
+        else is optional.
+        """
+        self._add_defaults_standards()
+        self._add_defaults_optional()
+        self._add_defaults_python()
+        self._add_defaults_data_files()
+        self._add_defaults_ext()
+        self._add_defaults_c_libs()
+        self._add_defaults_scripts()
+
+    @staticmethod
+    def _cs_path_exists(fspath):
+        """
+        Case-sensitive path existence check
+
+        >>> sdist._cs_path_exists(__file__)
+        True
+        >>> sdist._cs_path_exists(__file__.upper())
+        False
+        """
+        if not os.path.exists(fspath):
+            return False
+        # make absolute so we always have a directory
+        abspath = os.path.abspath(fspath)
+        directory, filename = os.path.split(abspath)
+        return filename in os.listdir(directory)
+
+    def _add_defaults_standards(self):
+        standards = [self.READMES, self.distribution.script_name]
+        for fn in standards:
+            if isinstance(fn, tuple):
+                alts = fn
+                got_it = False
+                for fn in alts:
+                    if self._cs_path_exists(fn):
+                        got_it = True
+                        self.filelist.append(fn)
+                        break
+
+                if not got_it:
+                    self.warn(
+                        "standard file not found: should have one of " + ', '.join(alts)
+                    )
+            else:
+                if self._cs_path_exists(fn):
+                    self.filelist.append(fn)
+                else:
+                    self.warn(f"standard file '{fn}' not found")
+
+    def _add_defaults_optional(self):
+        optional = ['tests/test*.py', 'test/test*.py', 'setup.cfg']
+        for pattern in optional:
+            files = filter(os.path.isfile, glob(pattern))
+            self.filelist.extend(files)
+
+    def _add_defaults_python(self):
+        # build_py is used to get:
+        #  - python modules
+        #  - files defined in package_data
+        build_py = self.get_finalized_command('build_py')
+
+        # getting python files
+        if self.distribution.has_pure_modules():
+            self.filelist.extend(build_py.get_source_files())
+
+        # getting package_data files
+        # (computed in build_py.data_files by build_py.finalize_options)
+        for _pkg, src_dir, _build_dir, filenames in build_py.data_files:
+            for filename in filenames:
+                self.filelist.append(os.path.join(src_dir, filename))
+
+    def _add_defaults_data_files(self):
+        # getting distribution.data_files
+        if self.distribution.has_data_files():
+            for item in self.distribution.data_files:
+                if isinstance(item, str):
+                    # plain file
+                    item = convert_path(item)
+                    if os.path.isfile(item):
+                        self.filelist.append(item)
+                else:
+                    # a (dirname, filenames) tuple
+                    dirname, filenames = item
+                    for f in filenames:
+                        f = convert_path(f)
+                        if os.path.isfile(f):
+                            self.filelist.append(f)
+
+    def _add_defaults_ext(self):
+        if self.distribution.has_ext_modules():
+            build_ext = self.get_finalized_command('build_ext')
+            self.filelist.extend(build_ext.get_source_files())
+
+    def _add_defaults_c_libs(self):
+        if self.distribution.has_c_libraries():
+            build_clib = self.get_finalized_command('build_clib')
+            self.filelist.extend(build_clib.get_source_files())
+
+    def _add_defaults_scripts(self):
+        if self.distribution.has_scripts():
+            build_scripts = self.get_finalized_command('build_scripts')
+            self.filelist.extend(build_scripts.get_source_files())
+
+    def read_template(self) -> None:
+        """Read and parse manifest template file named by self.template.
+
+        (usually "MANIFEST.in") The parsing and processing is done by
+        'self.filelist', which updates itself accordingly.
+        """
+        log.info("reading manifest template '%s'", self.template)
+        template = TextFile(
+            self.template,
+            strip_comments=True,
+            skip_blanks=True,
+            join_lines=True,
+            lstrip_ws=True,
+            rstrip_ws=True,
+            collapse_join=True,
+        )
+
+        try:
+            while True:
+                line = template.readline()
+                if line is None:  # end of file
+                    break
+
+                try:
+                    self.filelist.process_template_line(line)
+                # the call above can raise a DistutilsTemplateError for
+                # malformed lines, or a ValueError from the lower-level
+                # convert_path function
+                except (DistutilsTemplateError, ValueError) as msg:
+                    self.warn(
+                        f"{template.filename}, line {int(template.current_line)}: {msg}"
+                    )
+        finally:
+            template.close()
+
+    def prune_file_list(self) -> None:
+        """Prune off branches that might slip into the file list as created
+        by 'read_template()', but really don't belong there:
+          * the build tree (typically "build")
+          * the release tree itself (only an issue if we ran "sdist"
+            previously with --keep-temp, or it aborted)
+          * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories
+        """
+        build = self.get_finalized_command('build')
+        base_dir = self.distribution.get_fullname()
+
+        self.filelist.exclude_pattern(None, prefix=os.fspath(build.build_base))
+        self.filelist.exclude_pattern(None, prefix=base_dir)
+
+        if sys.platform == 'win32':
+            seps = r'/|\\'
+        else:
+            seps = '/'
+
+        vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr', '_darcs']
+        vcs_ptrn = r'(^|{})({})({}).*'.format(seps, '|'.join(vcs_dirs), seps)
+        self.filelist.exclude_pattern(vcs_ptrn, is_regex=True)
+
+    def write_manifest(self) -> None:
+        """Write the file list in 'self.filelist' (presumably as filled in
+        by 'add_defaults()' and 'read_template()') to the manifest file
+        named by 'self.manifest'.
+        """
+        if self._manifest_is_not_generated():
+            log.info(
+                f"not writing to manually maintained manifest file '{self.manifest}'"
+            )
+            return
+
+        content = self.filelist.files[:]
+        content.insert(0, '# file GENERATED by distutils, do NOT edit')
+        self.execute(
+            file_util.write_file,
+            (self.manifest, content),
+            f"writing manifest file '{self.manifest}'",
+        )
+
+    def _manifest_is_not_generated(self):
+        # check for special comment used in 3.1.3 and higher
+        if not os.path.isfile(self.manifest):
+            return False
+
+        with open(self.manifest, encoding='utf-8') as fp:
+            first_line = next(fp)
+        return first_line != '# file GENERATED by distutils, do NOT edit\n'
+
+    def read_manifest(self) -> None:
+        """Read the manifest file (named by 'self.manifest') and use it to
+        fill in 'self.filelist', the list of files to include in the source
+        distribution.
+        """
+        log.info("reading manifest file '%s'", self.manifest)
+        with open(self.manifest, encoding='utf-8') as lines:
+            self.filelist.extend(
+                # ignore comments and blank lines
+                filter(None, filterfalse(is_comment, map(str.strip, lines)))
+            )
+
+    def make_release_tree(self, base_dir, files) -> None:
+        """Create the directory tree that will become the source
+        distribution archive.  All directories implied by the filenames in
+        'files' are created under 'base_dir', and then we hard link or copy
+        (if hard linking is unavailable) those files into place.
+        Essentially, this duplicates the developer's source tree, but in a
+        directory named after the distribution, containing only the files
+        to be distributed.
+        """
+        # Create all the directories under 'base_dir' necessary to
+        # put 'files' there; the 'mkpath()' is just so we don't die
+        # if the manifest happens to be empty.
+        self.mkpath(base_dir)
+        dir_util.create_tree(base_dir, files, dry_run=self.dry_run)
+
+        # And walk over the list of files, either making a hard link (if
+        # os.link exists) to each one that doesn't already exist in its
+        # corresponding location under 'base_dir', or copying each file
+        # that's out-of-date in 'base_dir'.  (Usually, all files will be
+        # out-of-date, because by default we blow away 'base_dir' when
+        # we're done making the distribution archives.)
+
+        if hasattr(os, 'link'):  # can make hard links on this system
+            link = 'hard'
+            msg = f"making hard links in {base_dir}..."
+        else:  # nope, have to copy
+            link = None
+            msg = f"copying files to {base_dir}..."
+
+        if not files:
+            log.warning("no files to distribute -- empty manifest?")
+        else:
+            log.info(msg)
+        for file in files:
+            if not os.path.isfile(file):
+                log.warning("'%s' not a regular file -- skipping", file)
+            else:
+                dest = os.path.join(base_dir, file)
+                self.copy_file(file, dest, link=link)
+
+        self.distribution.metadata.write_pkg_info(base_dir)
+
+    def make_distribution(self) -> None:
+        """Create the source distribution(s).  First, we create the release
+        tree with 'make_release_tree()'; then, we create all required
+        archive files (according to 'self.formats') from the release tree.
+        Finally, we clean up by blowing away the release tree (unless
+        'self.keep_temp' is true).  The list of archive files created is
+        stored so it can be retrieved later by 'get_archive_files()'.
+        """
+        # Don't warn about missing meta-data here -- should be (and is!)
+        # done elsewhere.
+        base_dir = self.distribution.get_fullname()
+        base_name = os.path.join(self.dist_dir, base_dir)
+
+        self.make_release_tree(base_dir, self.filelist.files)
+        archive_files = []  # remember names of files we create
+        # tar archive must be created last to avoid overwrite and remove
+        if 'tar' in self.formats:
+            self.formats.append(self.formats.pop(self.formats.index('tar')))
+
+        for fmt in self.formats:
+            file = self.make_archive(
+                base_name, fmt, base_dir=base_dir, owner=self.owner, group=self.group
+            )
+            archive_files.append(file)
+            self.distribution.dist_files.append(('sdist', '', file))
+
+        self.archive_files = archive_files
+
+        if not self.keep_temp:
+            dir_util.remove_tree(base_dir, dry_run=self.dry_run)
+
+    def get_archive_files(self):
+        """Return the list of archive files created when the command
+        was run, or None if the command hasn't run yet.
+        """
+        return self.archive_files
+
+
+def is_comment(line: str) -> bool:
+    return line.startswith('#')
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/__init__.py
new file mode 100644
index 00000000..2c43729b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/__init__.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+from collections.abc import Iterable
+from typing import TypeVar
+
+_IterableT = TypeVar("_IterableT", bound="Iterable[str]")
+
+
+def consolidate_linker_args(args: _IterableT) -> _IterableT | str:
+    """
+    Ensure the return value is a string for backward compatibility.
+
+    Retain until at least 2025-04-31. See pypa/distutils#246
+    """
+
+    if not all(arg.startswith('-Wl,') for arg in args):
+        return args
+    return '-Wl,' + ','.join(arg.removeprefix('-Wl,') for arg in args)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/numpy.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/numpy.py
new file mode 100644
index 00000000..73eca7ac
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/numpy.py
@@ -0,0 +1,2 @@
+# required for older numpy versions on Pythons prior to 3.12; see pypa/setuptools#4876
+from ..compilers.C.base import _default_compilers, compiler_class  # noqa: F401
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/py39.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/py39.py
new file mode 100644
index 00000000..1b436d76
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compat/py39.py
@@ -0,0 +1,66 @@
+import functools
+import itertools
+import platform
+import sys
+
+
+def add_ext_suffix_39(vars):
+    """
+    Ensure vars contains 'EXT_SUFFIX'. pypa/distutils#130
+    """
+    import _imp
+
+    ext_suffix = _imp.extension_suffixes()[0]
+    vars.update(
+        EXT_SUFFIX=ext_suffix,
+        # sysconfig sets SO to match EXT_SUFFIX, so maintain
+        # that expectation.
+        # https://github.com/python/cpython/blob/785cc6770588de087d09e89a69110af2542be208/Lib/sysconfig.py#L671-L673
+        SO=ext_suffix,
+    )
+
+
+needs_ext_suffix = sys.version_info < (3, 10) and platform.system() == 'Windows'
+add_ext_suffix = add_ext_suffix_39 if needs_ext_suffix else lambda vars: None
+
+
+# from more_itertools
+class UnequalIterablesError(ValueError):
+    def __init__(self, details=None):
+        msg = 'Iterables have different lengths'
+        if details is not None:
+            msg += (': index 0 has length {}; index {} has length {}').format(*details)
+
+        super().__init__(msg)
+
+
+# from more_itertools
+def _zip_equal_generator(iterables):
+    _marker = object()
+    for combo in itertools.zip_longest(*iterables, fillvalue=_marker):
+        for val in combo:
+            if val is _marker:
+                raise UnequalIterablesError()
+        yield combo
+
+
+# from more_itertools
+def _zip_equal(*iterables):
+    # Check whether the iterables are all the same size.
+    try:
+        first_size = len(iterables[0])
+        for i, it in enumerate(iterables[1:], 1):
+            size = len(it)
+            if size != first_size:
+                raise UnequalIterablesError(details=(first_size, i, size))
+        # All sizes are equal, we can use the built-in zip.
+        return zip(*iterables)
+    # If any one of the iterables didn't have a length, start reading
+    # them until one runs out.
+    except TypeError:
+        return _zip_equal_generator(iterables)
+
+
+zip_strict = (
+    _zip_equal if sys.version_info < (3, 10) else functools.partial(zip, strict=True)
+)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/base.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/base.py
new file mode 100644
index 00000000..5efd2a39
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/base.py
@@ -0,0 +1,1394 @@
+"""distutils.ccompiler
+
+Contains Compiler, an abstract base class that defines the interface
+for the Distutils compiler abstraction model."""
+
+from __future__ import annotations
+
+import os
+import pathlib
+import re
+import sys
+import warnings
+from collections.abc import Callable, Iterable, MutableSequence, Sequence
+from typing import (
+    TYPE_CHECKING,
+    ClassVar,
+    Literal,
+    TypeVar,
+    Union,
+    overload,
+)
+
+from more_itertools import always_iterable
+
+from ..._log import log
+from ..._modified import newer_group
+from ...dir_util import mkpath
+from ...errors import (
+    DistutilsModuleError,
+    DistutilsPlatformError,
+)
+from ...file_util import move_file
+from ...spawn import spawn
+from ...util import execute, is_mingw, split_quoted
+from .errors import (
+    CompileError,
+    LinkError,
+    UnknownFileType,
+)
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias, TypeVarTuple, Unpack
+
+    _Ts = TypeVarTuple("_Ts")
+
+_Macro: TypeAlias = Union[tuple[str], tuple[str, Union[str, None]]]
+_StrPathT = TypeVar("_StrPathT", bound="str | os.PathLike[str]")
+_BytesPathT = TypeVar("_BytesPathT", bound="bytes | os.PathLike[bytes]")
+
+
+class Compiler:
+    """Abstract base class to define the interface that must be implemented
+    by real compiler classes.  Also has some utility methods used by
+    several compiler classes.
+
+    The basic idea behind a compiler abstraction class is that each
+    instance can be used for all the compile/link steps in building a
+    single project.  Thus, attributes common to all of those compile and
+    link steps -- include directories, macros to define, libraries to link
+    against, etc. -- are attributes of the compiler instance.  To allow for
+    variability in how individual files are treated, most of those
+    attributes may be varied on a per-compilation or per-link basis.
+    """
+
+    # 'compiler_type' is a class attribute that identifies this class.  It
+    # keeps code that wants to know what kind of compiler it's dealing with
+    # from having to import all possible compiler classes just to do an
+    # 'isinstance'.  In concrete CCompiler subclasses, 'compiler_type'
+    # should really, really be one of the keys of the 'compiler_class'
+    # dictionary (see below -- used by the 'new_compiler()' factory
+    # function) -- authors of new compiler interface classes are
+    # responsible for updating 'compiler_class'!
+    compiler_type: ClassVar[str] = None  # type: ignore[assignment]
+
+    # XXX things not handled by this compiler abstraction model:
+    #   * client can't provide additional options for a compiler,
+    #     e.g. warning, optimization, debugging flags.  Perhaps this
+    #     should be the domain of concrete compiler abstraction classes
+    #     (UnixCCompiler, MSVCCompiler, etc.) -- or perhaps the base
+    #     class should have methods for the common ones.
+    #   * can't completely override the include or library searchg
+    #     path, ie. no "cc -I -Idir1 -Idir2" or "cc -L -Ldir1 -Ldir2".
+    #     I'm not sure how widely supported this is even by Unix
+    #     compilers, much less on other platforms.  And I'm even less
+    #     sure how useful it is; maybe for cross-compiling, but
+    #     support for that is a ways off.  (And anyways, cross
+    #     compilers probably have a dedicated binary with the
+    #     right paths compiled in.  I hope.)
+    #   * can't do really freaky things with the library list/library
+    #     dirs, e.g. "-Ldir1 -lfoo -Ldir2 -lfoo" to link against
+    #     different versions of libfoo.a in different locations.  I
+    #     think this is useless without the ability to null out the
+    #     library search path anyways.
+
+    executables: ClassVar[dict]
+
+    # Subclasses that rely on the standard filename generation methods
+    # implemented below should override these; see the comment near
+    # those methods ('object_filenames()' et. al.) for details:
+    src_extensions: ClassVar[list[str] | None] = None
+    obj_extension: ClassVar[str | None] = None
+    static_lib_extension: ClassVar[str | None] = None
+    shared_lib_extension: ClassVar[str | None] = None
+    static_lib_format: ClassVar[str | None] = None  # format string
+    shared_lib_format: ClassVar[str | None] = None  # prob. same as static_lib_format
+    exe_extension: ClassVar[str | None] = None
+
+    # Default language settings. language_map is used to detect a source
+    # file or Extension target language, checking source filenames.
+    # language_order is used to detect the language precedence, when deciding
+    # what language to use when mixing source types. For example, if some
+    # extension has two files with ".c" extension, and one with ".cpp", it
+    # is still linked as c++.
+    language_map: ClassVar[dict[str, str]] = {
+        ".c": "c",
+        ".cc": "c++",
+        ".cpp": "c++",
+        ".cxx": "c++",
+        ".m": "objc",
+    }
+    language_order: ClassVar[list[str]] = ["c++", "objc", "c"]
+
+    include_dirs: list[str] = []
+    """
+    include dirs specific to this compiler class
+    """
+
+    library_dirs: list[str] = []
+    """
+    library dirs specific to this compiler class
+    """
+
+    def __init__(
+        self, verbose: bool = False, dry_run: bool = False, force: bool = False
+    ) -> None:
+        self.dry_run = dry_run
+        self.force = force
+        self.verbose = verbose
+
+        # 'output_dir': a common output directory for object, library,
+        # shared object, and shared library files
+        self.output_dir: str | None = None
+
+        # 'macros': a list of macro definitions (or undefinitions).  A
+        # macro definition is a 2-tuple (name, value), where the value is
+        # either a string or None (no explicit value).  A macro
+        # undefinition is a 1-tuple (name,).
+        self.macros: list[_Macro] = []
+
+        # 'include_dirs': a list of directories to search for include files
+        self.include_dirs = []
+
+        # 'libraries': a list of libraries to include in any link
+        # (library names, not filenames: eg. "foo" not "libfoo.a")
+        self.libraries: list[str] = []
+
+        # 'library_dirs': a list of directories to search for libraries
+        self.library_dirs = []
+
+        # 'runtime_library_dirs': a list of directories to search for
+        # shared libraries/objects at runtime
+        self.runtime_library_dirs: list[str] = []
+
+        # 'objects': a list of object files (or similar, such as explicitly
+        # named library files) to include on any link
+        self.objects: list[str] = []
+
+        for key in self.executables.keys():
+            self.set_executable(key, self.executables[key])
+
+    def set_executables(self, **kwargs: str) -> None:
+        """Define the executables (and options for them) that will be run
+        to perform the various stages of compilation.  The exact set of
+        executables that may be specified here depends on the compiler
+        class (via the 'executables' class attribute), but most will have:
+          compiler      the C/C++ compiler
+          linker_so     linker used to create shared objects and libraries
+          linker_exe    linker used to create binary executables
+          archiver      static library creator
+
+        On platforms with a command-line (Unix, DOS/Windows), each of these
+        is a string that will be split into executable name and (optional)
+        list of arguments.  (Splitting the string is done similarly to how
+        Unix shells operate: words are delimited by spaces, but quotes and
+        backslashes can override this.  See
+        'distutils.util.split_quoted()'.)
+        """
+
+        # Note that some CCompiler implementation classes will define class
+        # attributes 'cpp', 'cc', etc. with hard-coded executable names;
+        # this is appropriate when a compiler class is for exactly one
+        # compiler/OS combination (eg. MSVCCompiler).  Other compiler
+        # classes (UnixCCompiler, in particular) are driven by information
+        # discovered at run-time, since there are many different ways to do
+        # basically the same things with Unix C compilers.
+
+        for key in kwargs:
+            if key not in self.executables:
+                raise ValueError(
+                    f"unknown executable '{key}' for class {self.__class__.__name__}"
+                )
+            self.set_executable(key, kwargs[key])
+
+    def set_executable(self, key, value):
+        if isinstance(value, str):
+            setattr(self, key, split_quoted(value))
+        else:
+            setattr(self, key, value)
+
+    def _find_macro(self, name):
+        i = 0
+        for defn in self.macros:
+            if defn[0] == name:
+                return i
+            i += 1
+        return None
+
+    def _check_macro_definitions(self, definitions):
+        """Ensure that every element of 'definitions' is valid."""
+        for defn in definitions:
+            self._check_macro_definition(*defn)
+
+    def _check_macro_definition(self, defn):
+        """
+        Raise a TypeError if defn is not valid.
+
+        A valid definition is either a (name, value) 2-tuple or a (name,) tuple.
+        """
+        if not isinstance(defn, tuple) or not self._is_valid_macro(*defn):
+            raise TypeError(
+                f"invalid macro definition '{defn}': "
+                "must be tuple (string,), (string, string), or (string, None)"
+            )
+
+    @staticmethod
+    def _is_valid_macro(name, value=None):
+        """
+        A valid macro is a ``name : str`` and a ``value : str | None``.
+
+        >>> Compiler._is_valid_macro('foo', None)
+        True
+        """
+        return isinstance(name, str) and isinstance(value, (str, type(None)))
+
+    # -- Bookkeeping methods -------------------------------------------
+
+    def define_macro(self, name: str, value: str | None = None) -> None:
+        """Define a preprocessor macro for all compilations driven by this
+        compiler object.  The optional parameter 'value' should be a
+        string; if it is not supplied, then the macro will be defined
+        without an explicit value and the exact outcome depends on the
+        compiler used (XXX true? does ANSI say anything about this?)
+        """
+        # Delete from the list of macro definitions/undefinitions if
+        # already there (so that this one will take precedence).
+        i = self._find_macro(name)
+        if i is not None:
+            del self.macros[i]
+
+        self.macros.append((name, value))
+
+    def undefine_macro(self, name: str) -> None:
+        """Undefine a preprocessor macro for all compilations driven by
+        this compiler object.  If the same macro is defined by
+        'define_macro()' and undefined by 'undefine_macro()' the last call
+        takes precedence (including multiple redefinitions or
+        undefinitions).  If the macro is redefined/undefined on a
+        per-compilation basis (ie. in the call to 'compile()'), then that
+        takes precedence.
+        """
+        # Delete from the list of macro definitions/undefinitions if
+        # already there (so that this one will take precedence).
+        i = self._find_macro(name)
+        if i is not None:
+            del self.macros[i]
+
+        undefn = (name,)
+        self.macros.append(undefn)
+
+    def add_include_dir(self, dir: str) -> None:
+        """Add 'dir' to the list of directories that will be searched for
+        header files.  The compiler is instructed to search directories in
+        the order in which they are supplied by successive calls to
+        'add_include_dir()'.
+        """
+        self.include_dirs.append(dir)
+
+    def set_include_dirs(self, dirs: list[str]) -> None:
+        """Set the list of directories that will be searched to 'dirs' (a
+        list of strings).  Overrides any preceding calls to
+        'add_include_dir()'; subsequence calls to 'add_include_dir()' add
+        to the list passed to 'set_include_dirs()'.  This does not affect
+        any list of standard include directories that the compiler may
+        search by default.
+        """
+        self.include_dirs = dirs[:]
+
+    def add_library(self, libname: str) -> None:
+        """Add 'libname' to the list of libraries that will be included in
+        all links driven by this compiler object.  Note that 'libname'
+        should *not* be the name of a file containing a library, but the
+        name of the library itself: the actual filename will be inferred by
+        the linker, the compiler, or the compiler class (depending on the
+        platform).
+
+        The linker will be instructed to link against libraries in the
+        order they were supplied to 'add_library()' and/or
+        'set_libraries()'.  It is perfectly valid to duplicate library
+        names; the linker will be instructed to link against libraries as
+        many times as they are mentioned.
+        """
+        self.libraries.append(libname)
+
+    def set_libraries(self, libnames: list[str]) -> None:
+        """Set the list of libraries to be included in all links driven by
+        this compiler object to 'libnames' (a list of strings).  This does
+        not affect any standard system libraries that the linker may
+        include by default.
+        """
+        self.libraries = libnames[:]
+
+    def add_library_dir(self, dir: str) -> None:
+        """Add 'dir' to the list of directories that will be searched for
+        libraries specified to 'add_library()' and 'set_libraries()'.  The
+        linker will be instructed to search for libraries in the order they
+        are supplied to 'add_library_dir()' and/or 'set_library_dirs()'.
+        """
+        self.library_dirs.append(dir)
+
+    def set_library_dirs(self, dirs: list[str]) -> None:
+        """Set the list of library search directories to 'dirs' (a list of
+        strings).  This does not affect any standard library search path
+        that the linker may search by default.
+        """
+        self.library_dirs = dirs[:]
+
+    def add_runtime_library_dir(self, dir: str) -> None:
+        """Add 'dir' to the list of directories that will be searched for
+        shared libraries at runtime.
+        """
+        self.runtime_library_dirs.append(dir)
+
+    def set_runtime_library_dirs(self, dirs: list[str]) -> None:
+        """Set the list of directories to search for shared libraries at
+        runtime to 'dirs' (a list of strings).  This does not affect any
+        standard search path that the runtime linker may search by
+        default.
+        """
+        self.runtime_library_dirs = dirs[:]
+
+    def add_link_object(self, object: str) -> None:
+        """Add 'object' to the list of object files (or analogues, such as
+        explicitly named library files or the output of "resource
+        compilers") to be included in every link driven by this compiler
+        object.
+        """
+        self.objects.append(object)
+
+    def set_link_objects(self, objects: list[str]) -> None:
+        """Set the list of object files (or analogues) to be included in
+        every link to 'objects'.  This does not affect any standard object
+        files that the linker may include by default (such as system
+        libraries).
+        """
+        self.objects = objects[:]
+
+    # -- Private utility methods --------------------------------------
+    # (here for the convenience of subclasses)
+
+    # Helper method to prep compiler in subclass compile() methods
+
+    def _setup_compile(
+        self,
+        outdir: str | None,
+        macros: list[_Macro] | None,
+        incdirs: list[str] | tuple[str, ...] | None,
+        sources,
+        depends,
+        extra,
+    ):
+        """Process arguments and decide which source files to compile."""
+        outdir, macros, incdirs = self._fix_compile_args(outdir, macros, incdirs)
+
+        if extra is None:
+            extra = []
+
+        # Get the list of expected output (object) files
+        objects = self.object_filenames(sources, strip_dir=False, output_dir=outdir)
+        assert len(objects) == len(sources)
+
+        pp_opts = gen_preprocess_options(macros, incdirs)
+
+        build = {}
+        for i in range(len(sources)):
+            src = sources[i]
+            obj = objects[i]
+            ext = os.path.splitext(src)[1]
+            self.mkpath(os.path.dirname(obj))
+            build[obj] = (src, ext)
+
+        return macros, objects, extra, pp_opts, build
+
+    def _get_cc_args(self, pp_opts, debug, before):
+        # works for unixccompiler, cygwinccompiler
+        cc_args = pp_opts + ['-c']
+        if debug:
+            cc_args[:0] = ['-g']
+        if before:
+            cc_args[:0] = before
+        return cc_args
+
+    def _fix_compile_args(
+        self,
+        output_dir: str | None,
+        macros: list[_Macro] | None,
+        include_dirs: list[str] | tuple[str, ...] | None,
+    ) -> tuple[str, list[_Macro], list[str]]:
+        """Typecheck and fix-up some of the arguments to the 'compile()'
+        method, and return fixed-up values.  Specifically: if 'output_dir'
+        is None, replaces it with 'self.output_dir'; ensures that 'macros'
+        is a list, and augments it with 'self.macros'; ensures that
+        'include_dirs' is a list, and augments it with 'self.include_dirs'.
+        Guarantees that the returned values are of the correct type,
+        i.e. for 'output_dir' either string or None, and for 'macros' and
+        'include_dirs' either list or None.
+        """
+        if output_dir is None:
+            output_dir = self.output_dir
+        elif not isinstance(output_dir, str):
+            raise TypeError("'output_dir' must be a string or None")
+
+        if macros is None:
+            macros = list(self.macros)
+        elif isinstance(macros, list):
+            macros = macros + (self.macros or [])
+        else:
+            raise TypeError("'macros' (if supplied) must be a list of tuples")
+
+        if include_dirs is None:
+            include_dirs = list(self.include_dirs)
+        elif isinstance(include_dirs, (list, tuple)):
+            include_dirs = list(include_dirs) + (self.include_dirs or [])
+        else:
+            raise TypeError("'include_dirs' (if supplied) must be a list of strings")
+
+        # add include dirs for class
+        include_dirs += self.__class__.include_dirs
+
+        return output_dir, macros, include_dirs
+
+    def _prep_compile(self, sources, output_dir, depends=None):
+        """Decide which source files must be recompiled.
+
+        Determine the list of object files corresponding to 'sources',
+        and figure out which ones really need to be recompiled.
+        Return a list of all object files and a dictionary telling
+        which source files can be skipped.
+        """
+        # Get the list of expected output (object) files
+        objects = self.object_filenames(sources, output_dir=output_dir)
+        assert len(objects) == len(sources)
+
+        # Return an empty dict for the "which source files can be skipped"
+        # return value to preserve API compatibility.
+        return objects, {}
+
+    def _fix_object_args(
+        self, objects: list[str] | tuple[str, ...], output_dir: str | None
+    ) -> tuple[list[str], str]:
+        """Typecheck and fix up some arguments supplied to various methods.
+        Specifically: ensure that 'objects' is a list; if output_dir is
+        None, replace with self.output_dir.  Return fixed versions of
+        'objects' and 'output_dir'.
+        """
+        if not isinstance(objects, (list, tuple)):
+            raise TypeError("'objects' must be a list or tuple of strings")
+        objects = list(objects)
+
+        if output_dir is None:
+            output_dir = self.output_dir
+        elif not isinstance(output_dir, str):
+            raise TypeError("'output_dir' must be a string or None")
+
+        return (objects, output_dir)
+
+    def _fix_lib_args(
+        self,
+        libraries: list[str] | tuple[str, ...] | None,
+        library_dirs: list[str] | tuple[str, ...] | None,
+        runtime_library_dirs: list[str] | tuple[str, ...] | None,
+    ) -> tuple[list[str], list[str], list[str]]:
+        """Typecheck and fix up some of the arguments supplied to the
+        'link_*' methods.  Specifically: ensure that all arguments are
+        lists, and augment them with their permanent versions
+        (eg. 'self.libraries' augments 'libraries').  Return a tuple with
+        fixed versions of all arguments.
+        """
+        if libraries is None:
+            libraries = list(self.libraries)
+        elif isinstance(libraries, (list, tuple)):
+            libraries = list(libraries) + (self.libraries or [])
+        else:
+            raise TypeError("'libraries' (if supplied) must be a list of strings")
+
+        if library_dirs is None:
+            library_dirs = list(self.library_dirs)
+        elif isinstance(library_dirs, (list, tuple)):
+            library_dirs = list(library_dirs) + (self.library_dirs or [])
+        else:
+            raise TypeError("'library_dirs' (if supplied) must be a list of strings")
+
+        # add library dirs for class
+        library_dirs += self.__class__.library_dirs
+
+        if runtime_library_dirs is None:
+            runtime_library_dirs = list(self.runtime_library_dirs)
+        elif isinstance(runtime_library_dirs, (list, tuple)):
+            runtime_library_dirs = list(runtime_library_dirs) + (
+                self.runtime_library_dirs or []
+            )
+        else:
+            raise TypeError(
+                "'runtime_library_dirs' (if supplied) must be a list of strings"
+            )
+
+        return (libraries, library_dirs, runtime_library_dirs)
+
+    def _need_link(self, objects, output_file):
+        """Return true if we need to relink the files listed in 'objects'
+        to recreate 'output_file'.
+        """
+        if self.force:
+            return True
+        else:
+            if self.dry_run:
+                newer = newer_group(objects, output_file, missing='newer')
+            else:
+                newer = newer_group(objects, output_file)
+            return newer
+
+    def detect_language(self, sources: str | list[str]) -> str | None:
+        """Detect the language of a given file, or list of files. Uses
+        language_map, and language_order to do the job.
+        """
+        if not isinstance(sources, list):
+            sources = [sources]
+        lang = None
+        index = len(self.language_order)
+        for source in sources:
+            base, ext = os.path.splitext(source)
+            extlang = self.language_map.get(ext)
+            try:
+                extindex = self.language_order.index(extlang)
+                if extindex < index:
+                    lang = extlang
+                    index = extindex
+            except ValueError:
+                pass
+        return lang
+
+    # -- Worker methods ------------------------------------------------
+    # (must be implemented by subclasses)
+
+    def preprocess(
+        self,
+        source: str | os.PathLike[str],
+        output_file: str | os.PathLike[str] | None = None,
+        macros: list[_Macro] | None = None,
+        include_dirs: list[str] | tuple[str, ...] | None = None,
+        extra_preargs: list[str] | None = None,
+        extra_postargs: Iterable[str] | None = None,
+    ):
+        """Preprocess a single C/C++ source file, named in 'source'.
+        Output will be written to file named 'output_file', or stdout if
+        'output_file' not supplied.  'macros' is a list of macro
+        definitions as for 'compile()', which will augment the macros set
+        with 'define_macro()' and 'undefine_macro()'.  'include_dirs' is a
+        list of directory names that will be added to the default list.
+
+        Raises PreprocessError on failure.
+        """
+        pass
+
+    def compile(
+        self,
+        sources: Sequence[str | os.PathLike[str]],
+        output_dir: str | None = None,
+        macros: list[_Macro] | None = None,
+        include_dirs: list[str] | tuple[str, ...] | None = None,
+        debug: bool = False,
+        extra_preargs: list[str] | None = None,
+        extra_postargs: list[str] | None = None,
+        depends: list[str] | tuple[str, ...] | None = None,
+    ) -> list[str]:
+        """Compile one or more source files.
+
+        'sources' must be a list of filenames, most likely C/C++
+        files, but in reality anything that can be handled by a
+        particular compiler and compiler class (eg. MSVCCompiler can
+        handle resource files in 'sources').  Return a list of object
+        filenames, one per source filename in 'sources'.  Depending on
+        the implementation, not all source files will necessarily be
+        compiled, but all corresponding object filenames will be
+        returned.
+
+        If 'output_dir' is given, object files will be put under it, while
+        retaining their original path component.  That is, "foo/bar.c"
+        normally compiles to "foo/bar.o" (for a Unix implementation); if
+        'output_dir' is "build", then it would compile to
+        "build/foo/bar.o".
+
+        'macros', if given, must be a list of macro definitions.  A macro
+        definition is either a (name, value) 2-tuple or a (name,) 1-tuple.
+        The former defines a macro; if the value is None, the macro is
+        defined without an explicit value.  The 1-tuple case undefines a
+        macro.  Later definitions/redefinitions/ undefinitions take
+        precedence.
+
+        'include_dirs', if given, must be a list of strings, the
+        directories to add to the default include file search path for this
+        compilation only.
+
+        'debug' is a boolean; if true, the compiler will be instructed to
+        output debug symbols in (or alongside) the object file(s).
+
+        'extra_preargs' and 'extra_postargs' are implementation- dependent.
+        On platforms that have the notion of a command-line (e.g. Unix,
+        DOS/Windows), they are most likely lists of strings: extra
+        command-line arguments to prepend/append to the compiler command
+        line.  On other platforms, consult the implementation class
+        documentation.  In any event, they are intended as an escape hatch
+        for those occasions when the abstract compiler framework doesn't
+        cut the mustard.
+
+        'depends', if given, is a list of filenames that all targets
+        depend on.  If a source file is older than any file in
+        depends, then the source file will be recompiled.  This
+        supports dependency tracking, but only at a coarse
+        granularity.
+
+        Raises CompileError on failure.
+        """
+        # A concrete compiler class can either override this method
+        # entirely or implement _compile().
+        macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
+            output_dir, macros, include_dirs, sources, depends, extra_postargs
+        )
+        cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)
+
+        for obj in objects:
+            try:
+                src, ext = build[obj]
+            except KeyError:
+                continue
+            self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
+
+        # Return *all* object filenames, not just the ones we just built.
+        return objects
+
+    def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
+        """Compile 'src' to product 'obj'."""
+        # A concrete compiler class that does not override compile()
+        # should implement _compile().
+        pass
+
+    def create_static_lib(
+        self,
+        objects: list[str] | tuple[str, ...],
+        output_libname: str,
+        output_dir: str | None = None,
+        debug: bool = False,
+        target_lang: str | None = None,
+    ) -> None:
+        """Link a bunch of stuff together to create a static library file.
+        The "bunch of stuff" consists of the list of object files supplied
+        as 'objects', the extra object files supplied to
+        'add_link_object()' and/or 'set_link_objects()', the libraries
+        supplied to 'add_library()' and/or 'set_libraries()', and the
+        libraries supplied as 'libraries' (if any).
+
+        'output_libname' should be a library name, not a filename; the
+        filename will be inferred from the library name.  'output_dir' is
+        the directory where the library file will be put.
+
+        'debug' is a boolean; if true, debugging information will be
+        included in the library (note that on most platforms, it is the
+        compile step where this matters: the 'debug' flag is included here
+        just for consistency).
+
+        'target_lang' is the target language for which the given objects
+        are being compiled. This allows specific linkage time treatment of
+        certain languages.
+
+        Raises LibError on failure.
+        """
+        pass
+
+    # values for target_desc parameter in link()
+    SHARED_OBJECT = "shared_object"
+    SHARED_LIBRARY = "shared_library"
+    EXECUTABLE = "executable"
+
+    def link(
+        self,
+        target_desc: str,
+        objects: list[str] | tuple[str, ...],
+        output_filename: str,
+        output_dir: str | None = None,
+        libraries: list[str] | tuple[str, ...] | None = None,
+        library_dirs: list[str] | tuple[str, ...] | None = None,
+        runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
+        export_symbols: Iterable[str] | None = None,
+        debug: bool = False,
+        extra_preargs: list[str] | None = None,
+        extra_postargs: list[str] | None = None,
+        build_temp: str | os.PathLike[str] | None = None,
+        target_lang: str | None = None,
+    ):
+        """Link a bunch of stuff together to create an executable or
+        shared library file.
+
+        The "bunch of stuff" consists of the list of object files supplied
+        as 'objects'.  'output_filename' should be a filename.  If
+        'output_dir' is supplied, 'output_filename' is relative to it
+        (i.e. 'output_filename' can provide directory components if
+        needed).
+
+        'libraries' is a list of libraries to link against.  These are
+        library names, not filenames, since they're translated into
+        filenames in a platform-specific way (eg. "foo" becomes "libfoo.a"
+        on Unix and "foo.lib" on DOS/Windows).  However, they can include a
+        directory component, which means the linker will look in that
+        specific directory rather than searching all the normal locations.
+
+        'library_dirs', if supplied, should be a list of directories to
+        search for libraries that were specified as bare library names
+        (ie. no directory component).  These are on top of the system
+        default and those supplied to 'add_library_dir()' and/or
+        'set_library_dirs()'.  'runtime_library_dirs' is a list of
+        directories that will be embedded into the shared library and used
+        to search for other shared libraries that *it* depends on at
+        run-time.  (This may only be relevant on Unix.)
+
+        'export_symbols' is a list of symbols that the shared library will
+        export.  (This appears to be relevant only on Windows.)
+
+        'debug' is as for 'compile()' and 'create_static_lib()', with the
+        slight distinction that it actually matters on most platforms (as
+        opposed to 'create_static_lib()', which includes a 'debug' flag
+        mostly for form's sake).
+
+        'extra_preargs' and 'extra_postargs' are as for 'compile()' (except
+        of course that they supply command-line arguments for the
+        particular linker being used).
+
+        'target_lang' is the target language for which the given objects
+        are being compiled. This allows specific linkage time treatment of
+        certain languages.
+
+        Raises LinkError on failure.
+        """
+        raise NotImplementedError
+
+    # Old 'link_*()' methods, rewritten to use the new 'link()' method.
+
+    def link_shared_lib(
+        self,
+        objects: list[str] | tuple[str, ...],
+        output_libname: str,
+        output_dir: str | None = None,
+        libraries: list[str] | tuple[str, ...] | None = None,
+        library_dirs: list[str] | tuple[str, ...] | None = None,
+        runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
+        export_symbols: Iterable[str] | None = None,
+        debug: bool = False,
+        extra_preargs: list[str] | None = None,
+        extra_postargs: list[str] | None = None,
+        build_temp: str | os.PathLike[str] | None = None,
+        target_lang: str | None = None,
+    ):
+        self.link(
+            Compiler.SHARED_LIBRARY,
+            objects,
+            self.library_filename(output_libname, lib_type='shared'),
+            output_dir,
+            libraries,
+            library_dirs,
+            runtime_library_dirs,
+            export_symbols,
+            debug,
+            extra_preargs,
+            extra_postargs,
+            build_temp,
+            target_lang,
+        )
+
+    def link_shared_object(
+        self,
+        objects: list[str] | tuple[str, ...],
+        output_filename: str,
+        output_dir: str | None = None,
+        libraries: list[str] | tuple[str, ...] | None = None,
+        library_dirs: list[str] | tuple[str, ...] | None = None,
+        runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
+        export_symbols: Iterable[str] | None = None,
+        debug: bool = False,
+        extra_preargs: list[str] | None = None,
+        extra_postargs: list[str] | None = None,
+        build_temp: str | os.PathLike[str] | None = None,
+        target_lang: str | None = None,
+    ):
+        self.link(
+            Compiler.SHARED_OBJECT,
+            objects,
+            output_filename,
+            output_dir,
+            libraries,
+            library_dirs,
+            runtime_library_dirs,
+            export_symbols,
+            debug,
+            extra_preargs,
+            extra_postargs,
+            build_temp,
+            target_lang,
+        )
+
+    def link_executable(
+        self,
+        objects: list[str] | tuple[str, ...],
+        output_progname: str,
+        output_dir: str | None = None,
+        libraries: list[str] | tuple[str, ...] | None = None,
+        library_dirs: list[str] | tuple[str, ...] | None = None,
+        runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
+        debug: bool = False,
+        extra_preargs: list[str] | None = None,
+        extra_postargs: list[str] | None = None,
+        target_lang: str | None = None,
+    ):
+        self.link(
+            Compiler.EXECUTABLE,
+            objects,
+            self.executable_filename(output_progname),
+            output_dir,
+            libraries,
+            library_dirs,
+            runtime_library_dirs,
+            None,
+            debug,
+            extra_preargs,
+            extra_postargs,
+            None,
+            target_lang,
+        )
+
+    # -- Miscellaneous methods -----------------------------------------
+    # These are all used by the 'gen_lib_options() function; there is
+    # no appropriate default implementation so subclasses should
+    # implement all of these.
+
+    def library_dir_option(self, dir: str) -> str:
+        """Return the compiler option to add 'dir' to the list of
+        directories searched for libraries.
+        """
+        raise NotImplementedError
+
+    def runtime_library_dir_option(self, dir: str) -> str:
+        """Return the compiler option to add 'dir' to the list of
+        directories searched for runtime libraries.
+        """
+        raise NotImplementedError
+
+    def library_option(self, lib: str) -> str:
+        """Return the compiler option to add 'lib' to the list of libraries
+        linked into the shared library or executable.
+        """
+        raise NotImplementedError
+
+    def has_function(  # noqa: C901
+        self,
+        funcname: str,
+        includes: Iterable[str] | None = None,
+        include_dirs: list[str] | tuple[str, ...] | None = None,
+        libraries: list[str] | None = None,
+        library_dirs: list[str] | tuple[str, ...] | None = None,
+    ) -> bool:
+        """Return a boolean indicating whether funcname is provided as
+        a symbol on the current platform.  The optional arguments can
+        be used to augment the compilation environment.
+
+        The libraries argument is a list of flags to be passed to the
+        linker to make additional symbol definitions available for
+        linking.
+
+        The includes and include_dirs arguments are deprecated.
+        Usually, supplying include files with function declarations
+        will cause function detection to fail even in cases where the
+        symbol is available for linking.
+
+        """
+        # this can't be included at module scope because it tries to
+        # import math which might not be available at that point - maybe
+        # the necessary logic should just be inlined?
+        import tempfile
+
+        if includes is None:
+            includes = []
+        else:
+            warnings.warn("includes is deprecated", DeprecationWarning)
+        if include_dirs is None:
+            include_dirs = []
+        else:
+            warnings.warn("include_dirs is deprecated", DeprecationWarning)
+        if libraries is None:
+            libraries = []
+        if library_dirs is None:
+            library_dirs = []
+        fd, fname = tempfile.mkstemp(".c", funcname, text=True)
+        with os.fdopen(fd, "w", encoding='utf-8') as f:
+            for incl in includes:
+                f.write(f"""#include "{incl}"\n""")
+            if not includes:
+                # Use "char func(void);" as the prototype to follow
+                # what autoconf does.  This prototype does not match
+                # any well-known function the compiler might recognize
+                # as a builtin, so this ends up as a true link test.
+                # Without a fake prototype, the test would need to
+                # know the exact argument types, and the has_function
+                # interface does not provide that level of information.
+                f.write(
+                    f"""\
+#ifdef __cplusplus
+extern "C"
+#endif
+char {funcname}(void);
+"""
+                )
+            f.write(
+                f"""\
+int main (int argc, char **argv) {{
+    {funcname}();
+    return 0;
+}}
+"""
+            )
+
+        try:
+            objects = self.compile([fname], include_dirs=include_dirs)
+        except CompileError:
+            return False
+        finally:
+            os.remove(fname)
+
+        try:
+            self.link_executable(
+                objects, "a.out", libraries=libraries, library_dirs=library_dirs
+            )
+        except (LinkError, TypeError):
+            return False
+        else:
+            os.remove(
+                self.executable_filename("a.out", output_dir=self.output_dir or '')
+            )
+        finally:
+            for fn in objects:
+                os.remove(fn)
+        return True
+
+    def find_library_file(
+        self, dirs: Iterable[str], lib: str, debug: bool = False
+    ) -> str | None:
+        """Search the specified list of directories for a static or shared
+        library file 'lib' and return the full path to that file.  If
+        'debug' true, look for a debugging version (if that makes sense on
+        the current platform).  Return None if 'lib' wasn't found in any of
+        the specified directories.
+        """
+        raise NotImplementedError
+
+    # -- Filename generation methods -----------------------------------
+
+    # The default implementation of the filename generating methods are
+    # prejudiced towards the Unix/DOS/Windows view of the world:
+    #   * object files are named by replacing the source file extension
+    #     (eg. .c/.cpp -> .o/.obj)
+    #   * library files (shared or static) are named by plugging the
+    #     library name and extension into a format string, eg.
+    #     "lib%s.%s" % (lib_name, ".a") for Unix static libraries
+    #   * executables are named by appending an extension (possibly
+    #     empty) to the program name: eg. progname + ".exe" for
+    #     Windows
+    #
+    # To reduce redundant code, these methods expect to find
+    # several attributes in the current object (presumably defined
+    # as class attributes):
+    #   * src_extensions -
+    #     list of C/C++ source file extensions, eg. ['.c', '.cpp']
+    #   * obj_extension -
+    #     object file extension, eg. '.o' or '.obj'
+    #   * static_lib_extension -
+    #     extension for static library files, eg. '.a' or '.lib'
+    #   * shared_lib_extension -
+    #     extension for shared library/object files, eg. '.so', '.dll'
+    #   * static_lib_format -
+    #     format string for generating static library filenames,
+    #     eg. 'lib%s.%s' or '%s.%s'
+    #   * shared_lib_format
+    #     format string for generating shared library filenames
+    #     (probably same as static_lib_format, since the extension
+    #     is one of the intended parameters to the format string)
+    #   * exe_extension -
+    #     extension for executable files, eg. '' or '.exe'
+
+    def object_filenames(
+        self,
+        source_filenames: Iterable[str | os.PathLike[str]],
+        strip_dir: bool = False,
+        output_dir: str | os.PathLike[str] | None = '',
+    ) -> list[str]:
+        if output_dir is None:
+            output_dir = ''
+        return list(
+            self._make_out_path(output_dir, strip_dir, src_name)
+            for src_name in source_filenames
+        )
+
+    @property
+    def out_extensions(self):
+        return dict.fromkeys(self.src_extensions, self.obj_extension)
+
+    def _make_out_path(self, output_dir, strip_dir, src_name):
+        return self._make_out_path_exts(
+            output_dir, strip_dir, src_name, self.out_extensions
+        )
+
+    @classmethod
+    def _make_out_path_exts(cls, output_dir, strip_dir, src_name, extensions):
+        r"""
+        >>> exts = {'.c': '.o'}
+        >>> Compiler._make_out_path_exts('.', False, '/foo/bar.c', exts).replace('\\', '/')
+        './foo/bar.o'
+        >>> Compiler._make_out_path_exts('.', True, '/foo/bar.c', exts).replace('\\', '/')
+        './bar.o'
+        """
+        src = pathlib.PurePath(src_name)
+        # Ensure base is relative to honor output_dir (python/cpython#37775).
+        base = cls._make_relative(src)
+        try:
+            new_ext = extensions[src.suffix]
+        except LookupError:
+            raise UnknownFileType(f"unknown file type '{src.suffix}' (from '{src}')")
+        if strip_dir:
+            base = pathlib.PurePath(base.name)
+        return os.path.join(output_dir, base.with_suffix(new_ext))
+
+    @staticmethod
+    def _make_relative(base: pathlib.Path):
+        return base.relative_to(base.anchor)
+
+    @overload
+    def shared_object_filename(
+        self,
+        basename: str,
+        strip_dir: Literal[False] = False,
+        output_dir: str | os.PathLike[str] = "",
+    ) -> str: ...
+    @overload
+    def shared_object_filename(
+        self,
+        basename: str | os.PathLike[str],
+        strip_dir: Literal[True],
+        output_dir: str | os.PathLike[str] = "",
+    ) -> str: ...
+    def shared_object_filename(
+        self,
+        basename: str | os.PathLike[str],
+        strip_dir: bool = False,
+        output_dir: str | os.PathLike[str] = '',
+    ) -> str:
+        assert output_dir is not None
+        if strip_dir:
+            basename = os.path.basename(basename)
+        return os.path.join(output_dir, basename + self.shared_lib_extension)
+
+    @overload
+    def executable_filename(
+        self,
+        basename: str,
+        strip_dir: Literal[False] = False,
+        output_dir: str | os.PathLike[str] = "",
+    ) -> str: ...
+    @overload
+    def executable_filename(
+        self,
+        basename: str | os.PathLike[str],
+        strip_dir: Literal[True],
+        output_dir: str | os.PathLike[str] = "",
+    ) -> str: ...
+    def executable_filename(
+        self,
+        basename: str | os.PathLike[str],
+        strip_dir: bool = False,
+        output_dir: str | os.PathLike[str] = '',
+    ) -> str:
+        assert output_dir is not None
+        if strip_dir:
+            basename = os.path.basename(basename)
+        return os.path.join(output_dir, basename + (self.exe_extension or ''))
+
+    def library_filename(
+        self,
+        libname: str,
+        lib_type: str = "static",
+        strip_dir: bool = False,
+        output_dir: str | os.PathLike[str] = "",  # or 'shared'
+    ):
+        assert output_dir is not None
+        expected = '"static", "shared", "dylib", "xcode_stub"'
+        if lib_type not in eval(expected):
+            raise ValueError(f"'lib_type' must be {expected}")
+        fmt = getattr(self, lib_type + "_lib_format")
+        ext = getattr(self, lib_type + "_lib_extension")
+
+        dir, base = os.path.split(libname)
+        filename = fmt % (base, ext)
+        if strip_dir:
+            dir = ''
+
+        return os.path.join(output_dir, dir, filename)
+
+    # -- Utility methods -----------------------------------------------
+
+    def announce(self, msg: object, level: int = 1) -> None:
+        log.debug(msg)
+
+    def debug_print(self, msg: object) -> None:
+        from distutils.debug import DEBUG
+
+        if DEBUG:
+            print(msg)
+
+    def warn(self, msg: object) -> None:
+        sys.stderr.write(f"warning: {msg}\n")
+
+    def execute(
+        self,
+        func: Callable[[Unpack[_Ts]], object],
+        args: tuple[Unpack[_Ts]],
+        msg: object = None,
+        level: int = 1,
+    ) -> None:
+        execute(func, args, msg, self.dry_run)
+
+    def spawn(
+        self, cmd: MutableSequence[bytes | str | os.PathLike[str]], **kwargs
+    ) -> None:
+        spawn(cmd, dry_run=self.dry_run, **kwargs)
+
+    @overload
+    def move_file(
+        self, src: str | os.PathLike[str], dst: _StrPathT
+    ) -> _StrPathT | str: ...
+    @overload
+    def move_file(
+        self, src: bytes | os.PathLike[bytes], dst: _BytesPathT
+    ) -> _BytesPathT | bytes: ...
+    def move_file(
+        self,
+        src: str | os.PathLike[str] | bytes | os.PathLike[bytes],
+        dst: str | os.PathLike[str] | bytes | os.PathLike[bytes],
+    ) -> str | os.PathLike[str] | bytes | os.PathLike[bytes]:
+        return move_file(src, dst, dry_run=self.dry_run)
+
+    def mkpath(self, name, mode=0o777):
+        mkpath(name, mode, dry_run=self.dry_run)
+
+
+# Map a sys.platform/os.name ('posix', 'nt') to the default compiler
+# type for that platform. Keys are interpreted as re match
+# patterns. Order is important; platform mappings are preferred over
+# OS names.
+_default_compilers = (
+    # Platform string mappings
+    # on a cygwin built python we can use gcc like an ordinary UNIXish
+    # compiler
+    ('cygwin.*', 'unix'),
+    ('zos', 'zos'),
+    # OS name mappings
+    ('posix', 'unix'),
+    ('nt', 'msvc'),
+)
+
+
+def get_default_compiler(osname: str | None = None, platform: str | None = None) -> str:
+    """Determine the default compiler to use for the given platform.
+
+    osname should be one of the standard Python OS names (i.e. the
+    ones returned by os.name) and platform the common value
+    returned by sys.platform for the platform in question.
+
+    The default values are os.name and sys.platform in case the
+    parameters are not given.
+    """
+    if osname is None:
+        osname = os.name
+    if platform is None:
+        platform = sys.platform
+    # Mingw is a special case where sys.platform is 'win32' but we
+    # want to use the 'mingw32' compiler, so check it first
+    if is_mingw():
+        return 'mingw32'
+    for pattern, compiler in _default_compilers:
+        if (
+            re.match(pattern, platform) is not None
+            or re.match(pattern, osname) is not None
+        ):
+            return compiler
+    # Default to Unix compiler
+    return 'unix'
+
+
+# Map compiler types to (module_name, class_name) pairs -- ie. where to
+# find the code that implements an interface to this compiler.  (The module
+# is assumed to be in the 'distutils' package.)
+compiler_class = {
+    'unix': ('unixccompiler', 'UnixCCompiler', "standard UNIX-style compiler"),
+    'msvc': ('_msvccompiler', 'MSVCCompiler', "Microsoft Visual C++"),
+    'cygwin': (
+        'cygwinccompiler',
+        'CygwinCCompiler',
+        "Cygwin port of GNU C Compiler for Win32",
+    ),
+    'mingw32': (
+        'cygwinccompiler',
+        'Mingw32CCompiler',
+        "Mingw32 port of GNU C Compiler for Win32",
+    ),
+    'bcpp': ('bcppcompiler', 'BCPPCompiler', "Borland C++ Compiler"),
+    'zos': ('zosccompiler', 'zOSCCompiler', 'IBM XL C/C++ Compilers'),
+}
+
+
+def show_compilers() -> None:
+    """Print list of available compilers (used by the "--help-compiler"
+    options to "build", "build_ext", "build_clib").
+    """
+    # XXX this "knows" that the compiler option it's describing is
+    # "--compiler", which just happens to be the case for the three
+    # commands that use it.
+    from distutils.fancy_getopt import FancyGetopt
+
+    compilers = sorted(
+        ("compiler=" + compiler, None, compiler_class[compiler][2])
+        for compiler in compiler_class.keys()
+    )
+    pretty_printer = FancyGetopt(compilers)
+    pretty_printer.print_help("List of available compilers:")
+
+
+def new_compiler(
+    plat: str | None = None,
+    compiler: str | None = None,
+    verbose: bool = False,
+    dry_run: bool = False,
+    force: bool = False,
+) -> Compiler:
+    """Generate an instance of some CCompiler subclass for the supplied
+    platform/compiler combination.  'plat' defaults to 'os.name'
+    (eg. 'posix', 'nt'), and 'compiler' defaults to the default compiler
+    for that platform.  Currently only 'posix' and 'nt' are supported, and
+    the default compilers are "traditional Unix interface" (UnixCCompiler
+    class) and Visual C++ (MSVCCompiler class).  Note that it's perfectly
+    possible to ask for a Unix compiler object under Windows, and a
+    Microsoft compiler object under Unix -- if you supply a value for
+    'compiler', 'plat' is ignored.
+    """
+    if plat is None:
+        plat = os.name
+
+    try:
+        if compiler is None:
+            compiler = get_default_compiler(plat)
+
+        (module_name, class_name, long_description) = compiler_class[compiler]
+    except KeyError:
+        msg = f"don't know how to compile C/C++ code on platform '{plat}'"
+        if compiler is not None:
+            msg = msg + f" with '{compiler}' compiler"
+        raise DistutilsPlatformError(msg)
+
+    try:
+        module_name = "distutils." + module_name
+        __import__(module_name)
+        module = sys.modules[module_name]
+        klass = vars(module)[class_name]
+    except ImportError:
+        raise DistutilsModuleError(
+            f"can't compile C/C++ code: unable to load module '{module_name}'"
+        )
+    except KeyError:
+        raise DistutilsModuleError(
+            f"can't compile C/C++ code: unable to find class '{class_name}' "
+            f"in module '{module_name}'"
+        )
+
+    # XXX The None is necessary to preserve backwards compatibility
+    # with classes that expect verbose to be the first positional
+    # argument.
+    return klass(None, dry_run, force)
+
+
+def gen_preprocess_options(
+    macros: Iterable[_Macro], include_dirs: Iterable[str]
+) -> list[str]:
+    """Generate C pre-processor options (-D, -U, -I) as used by at least
+    two types of compilers: the typical Unix compiler and Visual C++.
+    'macros' is the usual thing, a list of 1- or 2-tuples, where (name,)
+    means undefine (-U) macro 'name', and (name,value) means define (-D)
+    macro 'name' to 'value'.  'include_dirs' is just a list of directory
+    names to be added to the header file search path (-I).  Returns a list
+    of command-line options suitable for either Unix compilers or Visual
+    C++.
+    """
+    # XXX it would be nice (mainly aesthetic, and so we don't generate
+    # stupid-looking command lines) to go over 'macros' and eliminate
+    # redundant definitions/undefinitions (ie. ensure that only the
+    # latest mention of a particular macro winds up on the command
+    # line).  I don't think it's essential, though, since most (all?)
+    # Unix C compilers only pay attention to the latest -D or -U
+    # mention of a macro on their command line.  Similar situation for
+    # 'include_dirs'.  I'm punting on both for now.  Anyways, weeding out
+    # redundancies like this should probably be the province of
+    # CCompiler, since the data structures used are inherited from it
+    # and therefore common to all CCompiler classes.
+    pp_opts = []
+    for macro in macros:
+        if not (isinstance(macro, tuple) and 1 <= len(macro) <= 2):
+            raise TypeError(
+                f"bad macro definition '{macro}': "
+                "each element of 'macros' list must be a 1- or 2-tuple"
+            )
+
+        if len(macro) == 1:  # undefine this macro
+            pp_opts.append(f"-U{macro[0]}")
+        elif len(macro) == 2:
+            if macro[1] is None:  # define with no explicit value
+                pp_opts.append(f"-D{macro[0]}")
+            else:
+                # XXX *don't* need to be clever about quoting the
+                # macro value here, because we're going to avoid the
+                # shell at all costs when we spawn the command!
+                pp_opts.append("-D{}={}".format(*macro))
+
+    pp_opts.extend(f"-I{dir}" for dir in include_dirs)
+    return pp_opts
+
+
+def gen_lib_options(
+    compiler: Compiler,
+    library_dirs: Iterable[str],
+    runtime_library_dirs: Iterable[str],
+    libraries: Iterable[str],
+) -> list[str]:
+    """Generate linker options for searching library directories and
+    linking with specific libraries.  'libraries' and 'library_dirs' are,
+    respectively, lists of library names (not filenames!) and search
+    directories.  Returns a list of command-line options suitable for use
+    with some compiler (depending on the two format strings passed in).
+    """
+    lib_opts = [compiler.library_dir_option(dir) for dir in library_dirs]
+
+    for dir in runtime_library_dirs:
+        lib_opts.extend(always_iterable(compiler.runtime_library_dir_option(dir)))
+
+    # XXX it's important that we *not* remove redundant library mentions!
+    # sometimes you really do have to say "-lfoo -lbar -lfoo" in order to
+    # resolve all symbols.  I just hope we never have to say "-lfoo obj.o
+    # -lbar" to get things to work -- that's certainly a possibility, but a
+    # pretty nasty way to arrange your C code.
+
+    for lib in libraries:
+        (lib_dir, lib_name) = os.path.split(lib)
+        if lib_dir:
+            lib_file = compiler.find_library_file([lib_dir], lib_name)
+            if lib_file:
+                lib_opts.append(lib_file)
+            else:
+                compiler.warn(
+                    f"no library file corresponding to '{lib}' found (skipping)"
+                )
+        else:
+            lib_opts.append(compiler.library_option(lib))
+    return lib_opts
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/cygwin.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/cygwin.py
new file mode 100644
index 00000000..bfabbb30
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/cygwin.py
@@ -0,0 +1,340 @@
+"""distutils.cygwinccompiler
+
+Provides the CygwinCCompiler class, a subclass of UnixCCompiler that
+handles the Cygwin port of the GNU C compiler to Windows.  It also contains
+the Mingw32CCompiler class which handles the mingw32 port of GCC (same as
+cygwin in no-cygwin mode).
+"""
+
+import copy
+import os
+import pathlib
+import shlex
+import sys
+import warnings
+from subprocess import check_output
+
+from ...errors import (
+    DistutilsExecError,
+    DistutilsPlatformError,
+)
+from ...file_util import write_file
+from ...sysconfig import get_config_vars
+from ...version import LooseVersion, suppress_known_deprecation
+from . import unix
+from .errors import (
+    CompileError,
+    Error,
+)
+
+
+def get_msvcr():
+    """No longer needed, but kept for backward compatibility."""
+    return []
+
+
+_runtime_library_dirs_msg = (
+    "Unable to set runtime library search path on Windows, "
+    "usually indicated by `runtime_library_dirs` parameter to Extension"
+)
+
+
+class Compiler(unix.Compiler):
+    """Handles the Cygwin port of the GNU C compiler to Windows."""
+
+    compiler_type = 'cygwin'
+    obj_extension = ".o"
+    static_lib_extension = ".a"
+    shared_lib_extension = ".dll.a"
+    dylib_lib_extension = ".dll"
+    static_lib_format = "lib%s%s"
+    shared_lib_format = "lib%s%s"
+    dylib_lib_format = "cyg%s%s"
+    exe_extension = ".exe"
+
+    def __init__(self, verbose=False, dry_run=False, force=False):
+        super().__init__(verbose, dry_run, force)
+
+        status, details = check_config_h()
+        self.debug_print(f"Python's GCC status: {status} (details: {details})")
+        if status is not CONFIG_H_OK:
+            self.warn(
+                "Python's pyconfig.h doesn't seem to support your compiler. "
+                f"Reason: {details}. "
+                "Compiling may fail because of undefined preprocessor macros."
+            )
+
+        self.cc, self.cxx = get_config_vars('CC', 'CXX')
+
+        # Override 'CC' and 'CXX' environment variables for
+        # building using MINGW compiler for MSVC python.
+        self.cc = os.environ.get('CC', self.cc or 'gcc')
+        self.cxx = os.environ.get('CXX', self.cxx or 'g++')
+
+        self.linker_dll = self.cc
+        self.linker_dll_cxx = self.cxx
+        shared_option = "-shared"
+
+        self.set_executables(
+            compiler=f'{self.cc} -mcygwin -O -Wall',
+            compiler_so=f'{self.cc} -mcygwin -mdll -O -Wall',
+            compiler_cxx=f'{self.cxx} -mcygwin -O -Wall',
+            compiler_so_cxx=f'{self.cxx} -mcygwin -mdll -O -Wall',
+            linker_exe=f'{self.cc} -mcygwin',
+            linker_so=f'{self.linker_dll} -mcygwin {shared_option}',
+            linker_exe_cxx=f'{self.cxx} -mcygwin',
+            linker_so_cxx=f'{self.linker_dll_cxx} -mcygwin {shared_option}',
+        )
+
+        self.dll_libraries = get_msvcr()
+
+    @property
+    def gcc_version(self):
+        # Older numpy depended on this existing to check for ancient
+        # gcc versions. This doesn't make much sense with clang etc so
+        # just hardcode to something recent.
+        # https://github.com/numpy/numpy/pull/20333
+        warnings.warn(
+            "gcc_version attribute of CygwinCCompiler is deprecated. "
+            "Instead of returning actual gcc version a fixed value 11.2.0 is returned.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        with suppress_known_deprecation():
+            return LooseVersion("11.2.0")
+
+    def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
+        """Compiles the source by spawning GCC and windres if needed."""
+        if ext in ('.rc', '.res'):
+            # gcc needs '.res' and '.rc' compiled to object files !!!
+            try:
+                self.spawn(["windres", "-i", src, "-o", obj])
+            except DistutilsExecError as msg:
+                raise CompileError(msg)
+        else:  # for other files use the C-compiler
+            try:
+                if self.detect_language(src) == 'c++':
+                    self.spawn(
+                        self.compiler_so_cxx
+                        + cc_args
+                        + [src, '-o', obj]
+                        + extra_postargs
+                    )
+                else:
+                    self.spawn(
+                        self.compiler_so + cc_args + [src, '-o', obj] + extra_postargs
+                    )
+            except DistutilsExecError as msg:
+                raise CompileError(msg)
+
+    def link(
+        self,
+        target_desc,
+        objects,
+        output_filename,
+        output_dir=None,
+        libraries=None,
+        library_dirs=None,
+        runtime_library_dirs=None,
+        export_symbols=None,
+        debug=False,
+        extra_preargs=None,
+        extra_postargs=None,
+        build_temp=None,
+        target_lang=None,
+    ):
+        """Link the objects."""
+        # use separate copies, so we can modify the lists
+        extra_preargs = copy.copy(extra_preargs or [])
+        libraries = copy.copy(libraries or [])
+        objects = copy.copy(objects or [])
+
+        if runtime_library_dirs:
+            self.warn(_runtime_library_dirs_msg)
+
+        # Additional libraries
+        libraries.extend(self.dll_libraries)
+
+        # handle export symbols by creating a def-file
+        # with executables this only works with gcc/ld as linker
+        if (export_symbols is not None) and (
+            target_desc != self.EXECUTABLE or self.linker_dll == "gcc"
+        ):
+            # (The linker doesn't do anything if output is up-to-date.
+            # So it would probably better to check if we really need this,
+            # but for this we had to insert some unchanged parts of
+            # UnixCCompiler, and this is not what we want.)
+
+            # we want to put some files in the same directory as the
+            # object files are, build_temp doesn't help much
+            # where are the object files
+            temp_dir = os.path.dirname(objects[0])
+            # name of dll to give the helper files the same base name
+            (dll_name, dll_extension) = os.path.splitext(
+                os.path.basename(output_filename)
+            )
+
+            # generate the filenames for these files
+            def_file = os.path.join(temp_dir, dll_name + ".def")
+
+            # Generate .def file
+            contents = [f"LIBRARY {os.path.basename(output_filename)}", "EXPORTS"]
+            contents.extend(export_symbols)
+            self.execute(write_file, (def_file, contents), f"writing {def_file}")
+
+            # next add options for def-file
+
+            # for gcc/ld the def-file is specified as any object files
+            objects.append(def_file)
+
+        # end: if ((export_symbols is not None) and
+        #        (target_desc != self.EXECUTABLE or self.linker_dll == "gcc")):
+
+        # who wants symbols and a many times larger output file
+        # should explicitly switch the debug mode on
+        # otherwise we let ld strip the output file
+        # (On my machine: 10KiB < stripped_file < ??100KiB
+        #   unstripped_file = stripped_file + XXX KiB
+        #  ( XXX=254 for a typical python extension))
+        if not debug:
+            extra_preargs.append("-s")
+
+        super().link(
+            target_desc,
+            objects,
+            output_filename,
+            output_dir,
+            libraries,
+            library_dirs,
+            runtime_library_dirs,
+            None,  # export_symbols, we do this in our def-file
+            debug,
+            extra_preargs,
+            extra_postargs,
+            build_temp,
+            target_lang,
+        )
+
+    def runtime_library_dir_option(self, dir):
+        # cygwin doesn't support rpath. While in theory we could error
+        # out like MSVC does, code might expect it to work like on Unix, so
+        # just warn and hope for the best.
+        self.warn(_runtime_library_dirs_msg)
+        return []
+
+    # -- Miscellaneous methods -----------------------------------------
+
+    def _make_out_path(self, output_dir, strip_dir, src_name):
+        # use normcase to make sure '.rc' is really '.rc' and not '.RC'
+        norm_src_name = os.path.normcase(src_name)
+        return super()._make_out_path(output_dir, strip_dir, norm_src_name)
+
+    @property
+    def out_extensions(self):
+        """
+        Add support for rc and res files.
+        """
+        return {
+            **super().out_extensions,
+            **{ext: ext + self.obj_extension for ext in ('.res', '.rc')},
+        }
+
+
+# the same as cygwin plus some additional parameters
+class MinGW32Compiler(Compiler):
+    """Handles the Mingw32 port of the GNU C compiler to Windows."""
+
+    compiler_type = 'mingw32'
+
+    def __init__(self, verbose=False, dry_run=False, force=False):
+        super().__init__(verbose, dry_run, force)
+
+        shared_option = "-shared"
+
+        if is_cygwincc(self.cc):
+            raise Error('Cygwin gcc cannot be used with --compiler=mingw32')
+
+        self.set_executables(
+            compiler=f'{self.cc} -O -Wall',
+            compiler_so=f'{self.cc} -shared -O -Wall',
+            compiler_so_cxx=f'{self.cxx} -shared -O -Wall',
+            compiler_cxx=f'{self.cxx} -O -Wall',
+            linker_exe=f'{self.cc}',
+            linker_so=f'{self.linker_dll} {shared_option}',
+            linker_exe_cxx=f'{self.cxx}',
+            linker_so_cxx=f'{self.linker_dll_cxx} {shared_option}',
+        )
+
+    def runtime_library_dir_option(self, dir):
+        raise DistutilsPlatformError(_runtime_library_dirs_msg)
+
+
+# Because these compilers aren't configured in Python's pyconfig.h file by
+# default, we should at least warn the user if he is using an unmodified
+# version.
+
+CONFIG_H_OK = "ok"
+CONFIG_H_NOTOK = "not ok"
+CONFIG_H_UNCERTAIN = "uncertain"
+
+
+def check_config_h():
+    """Check if the current Python installation appears amenable to building
+    extensions with GCC.
+
+    Returns a tuple (status, details), where 'status' is one of the following
+    constants:
+
+    - CONFIG_H_OK: all is well, go ahead and compile
+    - CONFIG_H_NOTOK: doesn't look good
+    - CONFIG_H_UNCERTAIN: not sure -- unable to read pyconfig.h
+
+    'details' is a human-readable string explaining the situation.
+
+    Note there are two ways to conclude "OK": either 'sys.version' contains
+    the string "GCC" (implying that this Python was built with GCC), or the
+    installed "pyconfig.h" contains the string "__GNUC__".
+    """
+
+    # XXX since this function also checks sys.version, it's not strictly a
+    # "pyconfig.h" check -- should probably be renamed...
+
+    from distutils import sysconfig
+
+    # if sys.version contains GCC then python was compiled with GCC, and the
+    # pyconfig.h file should be OK
+    if "GCC" in sys.version:
+        return CONFIG_H_OK, "sys.version mentions 'GCC'"
+
+    # Clang would also work
+    if "Clang" in sys.version:
+        return CONFIG_H_OK, "sys.version mentions 'Clang'"
+
+    # let's see if __GNUC__ is mentioned in python.h
+    fn = sysconfig.get_config_h_filename()
+    try:
+        config_h = pathlib.Path(fn).read_text(encoding='utf-8')
+    except OSError as exc:
+        return (CONFIG_H_UNCERTAIN, f"couldn't read '{fn}': {exc.strerror}")
+    else:
+        substring = '__GNUC__'
+        if substring in config_h:
+            code = CONFIG_H_OK
+            mention_inflected = 'mentions'
+        else:
+            code = CONFIG_H_NOTOK
+            mention_inflected = 'does not mention'
+        return code, f"{fn!r} {mention_inflected} {substring!r}"
+
+
+def is_cygwincc(cc):
+    """Try to determine if the compiler that would be used is from cygwin."""
+    out_string = check_output(shlex.split(cc) + ['-dumpmachine'])
+    return out_string.strip().endswith(b'cygwin')
+
+
+get_versions = None
+"""
+A stand-in for the previous get_versions() function to prevent failures
+when monkeypatched. See pypa/setuptools#2969.
+"""
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/errors.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/errors.py
new file mode 100644
index 00000000..01328592
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/errors.py
@@ -0,0 +1,24 @@
+class Error(Exception):
+    """Some compile/link operation failed."""
+
+
+class PreprocessError(Error):
+    """Failure to preprocess one or more C/C++ files."""
+
+
+class CompileError(Error):
+    """Failure to compile one or more C/C++ source files."""
+
+
+class LibError(Error):
+    """Failure to create a static library from one or more C/C++ object
+    files."""
+
+
+class LinkError(Error):
+    """Failure to link one or more C/C++ object files into an executable
+    or shared library file."""
+
+
+class UnknownFileType(Error):
+    """Attempt to process an unknown file type."""
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/msvc.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/msvc.py
new file mode 100644
index 00000000..6db062a9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/msvc.py
@@ -0,0 +1,614 @@
+"""distutils._msvccompiler
+
+Contains MSVCCompiler, an implementation of the abstract CCompiler class
+for Microsoft Visual Studio 2015.
+
+This module requires VS 2015 or later.
+"""
+
+# Written by Perry Stoll
+# hacked by Robin Becker and Thomas Heller to do a better job of
+#   finding DevStudio (through the registry)
+# ported to VS 2005 and VS 2008 by Christian Heimes
+# ported to VS 2015 by Steve Dower
+from __future__ import annotations
+
+import contextlib
+import os
+import subprocess
+import unittest.mock as mock
+import warnings
+from collections.abc import Iterable
+
+with contextlib.suppress(ImportError):
+    import winreg
+
+from itertools import count
+
+from ..._log import log
+from ...errors import (
+    DistutilsExecError,
+    DistutilsPlatformError,
+)
+from ...util import get_host_platform, get_platform
+from . import base
+from .base import gen_lib_options
+from .errors import (
+    CompileError,
+    LibError,
+    LinkError,
+)
+
+
+def _find_vc2015():
+    try:
+        key = winreg.OpenKeyEx(
+            winreg.HKEY_LOCAL_MACHINE,
+            r"Software\Microsoft\VisualStudio\SxS\VC7",
+            access=winreg.KEY_READ | winreg.KEY_WOW64_32KEY,
+        )
+    except OSError:
+        log.debug("Visual C++ is not registered")
+        return None, None
+
+    best_version = 0
+    best_dir = None
+    with key:
+        for i in count():
+            try:
+                v, vc_dir, vt = winreg.EnumValue(key, i)
+            except OSError:
+                break
+            if v and vt == winreg.REG_SZ and os.path.isdir(vc_dir):
+                try:
+                    version = int(float(v))
+                except (ValueError, TypeError):
+                    continue
+                if version >= 14 and version > best_version:
+                    best_version, best_dir = version, vc_dir
+    return best_version, best_dir
+
+
+def _find_vc2017():
+    """Returns "15, path" based on the result of invoking vswhere.exe
+    If no install is found, returns "None, None"
+
+    The version is returned to avoid unnecessarily changing the function
+    result. It may be ignored when the path is not None.
+
+    If vswhere.exe is not available, by definition, VS 2017 is not
+    installed.
+    """
+    root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
+    if not root:
+        return None, None
+
+    variant = 'arm64' if get_platform() == 'win-arm64' else 'x86.x64'
+    suitable_components = (
+        f"Microsoft.VisualStudio.Component.VC.Tools.{variant}",
+        "Microsoft.VisualStudio.Workload.WDExpress",
+    )
+
+    for component in suitable_components:
+        # Workaround for `-requiresAny` (only available on VS 2017 > 15.6)
+        with contextlib.suppress(
+            subprocess.CalledProcessError, OSError, UnicodeDecodeError
+        ):
+            path = (
+                subprocess.check_output([
+                    os.path.join(
+                        root, "Microsoft Visual Studio", "Installer", "vswhere.exe"
+                    ),
+                    "-latest",
+                    "-prerelease",
+                    "-requires",
+                    component,
+                    "-property",
+                    "installationPath",
+                    "-products",
+                    "*",
+                ])
+                .decode(encoding="mbcs", errors="strict")
+                .strip()
+            )
+
+            path = os.path.join(path, "VC", "Auxiliary", "Build")
+            if os.path.isdir(path):
+                return 15, path
+
+    return None, None  # no suitable component found
+
+
+PLAT_SPEC_TO_RUNTIME = {
+    'x86': 'x86',
+    'x86_amd64': 'x64',
+    'x86_arm': 'arm',
+    'x86_arm64': 'arm64',
+}
+
+
+def _find_vcvarsall(plat_spec):
+    # bpo-38597: Removed vcruntime return value
+    _, best_dir = _find_vc2017()
+
+    if not best_dir:
+        best_version, best_dir = _find_vc2015()
+
+    if not best_dir:
+        log.debug("No suitable Visual C++ version found")
+        return None, None
+
+    vcvarsall = os.path.join(best_dir, "vcvarsall.bat")
+    if not os.path.isfile(vcvarsall):
+        log.debug("%s cannot be found", vcvarsall)
+        return None, None
+
+    return vcvarsall, None
+
+
+def _get_vc_env(plat_spec):
+    if os.getenv("DISTUTILS_USE_SDK"):
+        return {key.lower(): value for key, value in os.environ.items()}
+
+    vcvarsall, _ = _find_vcvarsall(plat_spec)
+    if not vcvarsall:
+        raise DistutilsPlatformError(
+            'Microsoft Visual C++ 14.0 or greater is required. '
+            'Get it with "Microsoft C++ Build Tools": '
+            'https://visualstudio.microsoft.com/visual-cpp-build-tools/'
+        )
+
+    try:
+        out = subprocess.check_output(
+            f'cmd /u /c "{vcvarsall}" {plat_spec} && set',
+            stderr=subprocess.STDOUT,
+        ).decode('utf-16le', errors='replace')
+    except subprocess.CalledProcessError as exc:
+        log.error(exc.output)
+        raise DistutilsPlatformError(f"Error executing {exc.cmd}")
+
+    env = {
+        key.lower(): value
+        for key, _, value in (line.partition('=') for line in out.splitlines())
+        if key and value
+    }
+
+    return env
+
+
+def _find_exe(exe, paths=None):
+    """Return path to an MSVC executable program.
+
+    Tries to find the program in several places: first, one of the
+    MSVC program search paths from the registry; next, the directories
+    in the PATH environment variable.  If any of those work, return an
+    absolute path that is known to exist.  If none of them work, just
+    return the original program name, 'exe'.
+    """
+    if not paths:
+        paths = os.getenv('path').split(os.pathsep)
+    for p in paths:
+        fn = os.path.join(os.path.abspath(p), exe)
+        if os.path.isfile(fn):
+            return fn
+    return exe
+
+
+_vcvars_names = {
+    'win32': 'x86',
+    'win-amd64': 'amd64',
+    'win-arm32': 'arm',
+    'win-arm64': 'arm64',
+}
+
+
+def _get_vcvars_spec(host_platform, platform):
+    """
+    Given a host platform and platform, determine the spec for vcvarsall.
+
+    Uses the native MSVC host if the host platform would need expensive
+    emulation for x86.
+
+    >>> _get_vcvars_spec('win-arm64', 'win32')
+    'arm64_x86'
+    >>> _get_vcvars_spec('win-arm64', 'win-amd64')
+    'arm64_amd64'
+
+    Otherwise, always cross-compile from x86 to work with the
+    lighter-weight MSVC installs that do not include native 64-bit tools.
+
+    >>> _get_vcvars_spec('win32', 'win32')
+    'x86'
+    >>> _get_vcvars_spec('win-arm32', 'win-arm32')
+    'x86_arm'
+    >>> _get_vcvars_spec('win-amd64', 'win-arm64')
+    'x86_arm64'
+    """
+    if host_platform != 'win-arm64':
+        host_platform = 'win32'
+    vc_hp = _vcvars_names[host_platform]
+    vc_plat = _vcvars_names[platform]
+    return vc_hp if vc_hp == vc_plat else f'{vc_hp}_{vc_plat}'
+
+
+class Compiler(base.Compiler):
+    """Concrete class that implements an interface to Microsoft Visual C++,
+    as defined by the CCompiler abstract class."""
+
+    compiler_type = 'msvc'
+
+    # Just set this so CCompiler's constructor doesn't barf.  We currently
+    # don't use the 'set_executables()' bureaucracy provided by CCompiler,
+    # as it really isn't necessary for this sort of single-compiler class.
+    # Would be nice to have a consistent interface with UnixCCompiler,
+    # though, so it's worth thinking about.
+    executables = {}
+
+    # Private class data (need to distinguish C from C++ source for compiler)
+    _c_extensions = ['.c']
+    _cpp_extensions = ['.cc', '.cpp', '.cxx']
+    _rc_extensions = ['.rc']
+    _mc_extensions = ['.mc']
+
+    # Needed for the filename generation methods provided by the
+    # base class, CCompiler.
+    src_extensions = _c_extensions + _cpp_extensions + _rc_extensions + _mc_extensions
+    res_extension = '.res'
+    obj_extension = '.obj'
+    static_lib_extension = '.lib'
+    shared_lib_extension = '.dll'
+    static_lib_format = shared_lib_format = '%s%s'
+    exe_extension = '.exe'
+
+    def __init__(self, verbose=False, dry_run=False, force=False) -> None:
+        super().__init__(verbose, dry_run, force)
+        # target platform (.plat_name is consistent with 'bdist')
+        self.plat_name = None
+        self.initialized = False
+
+    @classmethod
+    def _configure(cls, vc_env):
+        """
+        Set class-level include/lib dirs.
+        """
+        cls.include_dirs = cls._parse_path(vc_env.get('include', ''))
+        cls.library_dirs = cls._parse_path(vc_env.get('lib', ''))
+
+    @staticmethod
+    def _parse_path(val):
+        return [dir.rstrip(os.sep) for dir in val.split(os.pathsep) if dir]
+
+    def initialize(self, plat_name: str | None = None) -> None:
+        # multi-init means we would need to check platform same each time...
+        assert not self.initialized, "don't init multiple times"
+        if plat_name is None:
+            plat_name = get_platform()
+        # sanity check for platforms to prevent obscure errors later.
+        if plat_name not in _vcvars_names:
+            raise DistutilsPlatformError(
+                f"--plat-name must be one of {tuple(_vcvars_names)}"
+            )
+
+        plat_spec = _get_vcvars_spec(get_host_platform(), plat_name)
+
+        vc_env = _get_vc_env(plat_spec)
+        if not vc_env:
+            raise DistutilsPlatformError(
+                "Unable to find a compatible Visual Studio installation."
+            )
+        self._configure(vc_env)
+
+        self._paths = vc_env.get('path', '')
+        paths = self._paths.split(os.pathsep)
+        self.cc = _find_exe("cl.exe", paths)
+        self.linker = _find_exe("link.exe", paths)
+        self.lib = _find_exe("lib.exe", paths)
+        self.rc = _find_exe("rc.exe", paths)  # resource compiler
+        self.mc = _find_exe("mc.exe", paths)  # message compiler
+        self.mt = _find_exe("mt.exe", paths)  # message compiler
+
+        self.preprocess_options = None
+        # bpo-38597: Always compile with dynamic linking
+        # Future releases of Python 3.x will include all past
+        # versions of vcruntime*.dll for compatibility.
+        self.compile_options = ['/nologo', '/O2', '/W3', '/GL', '/DNDEBUG', '/MD']
+
+        self.compile_options_debug = [
+            '/nologo',
+            '/Od',
+            '/MDd',
+            '/Zi',
+            '/W3',
+            '/D_DEBUG',
+        ]
+
+        ldflags = ['/nologo', '/INCREMENTAL:NO', '/LTCG']
+
+        ldflags_debug = ['/nologo', '/INCREMENTAL:NO', '/LTCG', '/DEBUG:FULL']
+
+        self.ldflags_exe = [*ldflags, '/MANIFEST:EMBED,ID=1']
+        self.ldflags_exe_debug = [*ldflags_debug, '/MANIFEST:EMBED,ID=1']
+        self.ldflags_shared = [
+            *ldflags,
+            '/DLL',
+            '/MANIFEST:EMBED,ID=2',
+            '/MANIFESTUAC:NO',
+        ]
+        self.ldflags_shared_debug = [
+            *ldflags_debug,
+            '/DLL',
+            '/MANIFEST:EMBED,ID=2',
+            '/MANIFESTUAC:NO',
+        ]
+        self.ldflags_static = [*ldflags]
+        self.ldflags_static_debug = [*ldflags_debug]
+
+        self._ldflags = {
+            (base.Compiler.EXECUTABLE, None): self.ldflags_exe,
+            (base.Compiler.EXECUTABLE, False): self.ldflags_exe,
+            (base.Compiler.EXECUTABLE, True): self.ldflags_exe_debug,
+            (base.Compiler.SHARED_OBJECT, None): self.ldflags_shared,
+            (base.Compiler.SHARED_OBJECT, False): self.ldflags_shared,
+            (base.Compiler.SHARED_OBJECT, True): self.ldflags_shared_debug,
+            (base.Compiler.SHARED_LIBRARY, None): self.ldflags_static,
+            (base.Compiler.SHARED_LIBRARY, False): self.ldflags_static,
+            (base.Compiler.SHARED_LIBRARY, True): self.ldflags_static_debug,
+        }
+
+        self.initialized = True
+
+    # -- Worker methods ------------------------------------------------
+
+    @property
+    def out_extensions(self) -> dict[str, str]:
+        return {
+            **super().out_extensions,
+            **{
+                ext: self.res_extension
+                for ext in self._rc_extensions + self._mc_extensions
+            },
+        }
+
+    def compile(  # noqa: C901
+        self,
+        sources,
+        output_dir=None,
+        macros=None,
+        include_dirs=None,
+        debug=False,
+        extra_preargs=None,
+        extra_postargs=None,
+        depends=None,
+    ):
+        if not self.initialized:
+            self.initialize()
+        compile_info = self._setup_compile(
+            output_dir, macros, include_dirs, sources, depends, extra_postargs
+        )
+        macros, objects, extra_postargs, pp_opts, build = compile_info
+
+        compile_opts = extra_preargs or []
+        compile_opts.append('/c')
+        if debug:
+            compile_opts.extend(self.compile_options_debug)
+        else:
+            compile_opts.extend(self.compile_options)
+
+        add_cpp_opts = False
+
+        for obj in objects:
+            try:
+                src, ext = build[obj]
+            except KeyError:
+                continue
+            if debug:
+                # pass the full pathname to MSVC in debug mode,
+                # this allows the debugger to find the source file
+                # without asking the user to browse for it
+                src = os.path.abspath(src)
+
+            if ext in self._c_extensions:
+                input_opt = f"/Tc{src}"
+            elif ext in self._cpp_extensions:
+                input_opt = f"/Tp{src}"
+                add_cpp_opts = True
+            elif ext in self._rc_extensions:
+                # compile .RC to .RES file
+                input_opt = src
+                output_opt = "/fo" + obj
+                try:
+                    self.spawn([self.rc] + pp_opts + [output_opt, input_opt])
+                except DistutilsExecError as msg:
+                    raise CompileError(msg)
+                continue
+            elif ext in self._mc_extensions:
+                # Compile .MC to .RC file to .RES file.
+                #   * '-h dir' specifies the directory for the
+                #     generated include file
+                #   * '-r dir' specifies the target directory of the
+                #     generated RC file and the binary message resource
+                #     it includes
+                #
+                # For now (since there are no options to change this),
+                # we use the source-directory for the include file and
+                # the build directory for the RC file and message
+                # resources. This works at least for win32all.
+                h_dir = os.path.dirname(src)
+                rc_dir = os.path.dirname(obj)
+                try:
+                    # first compile .MC to .RC and .H file
+                    self.spawn([self.mc, '-h', h_dir, '-r', rc_dir, src])
+                    base, _ = os.path.splitext(os.path.basename(src))
+                    rc_file = os.path.join(rc_dir, base + '.rc')
+                    # then compile .RC to .RES file
+                    self.spawn([self.rc, "/fo" + obj, rc_file])
+
+                except DistutilsExecError as msg:
+                    raise CompileError(msg)
+                continue
+            else:
+                # how to handle this file?
+                raise CompileError(f"Don't know how to compile {src} to {obj}")
+
+            args = [self.cc] + compile_opts + pp_opts
+            if add_cpp_opts:
+                args.append('/EHsc')
+            args.extend((input_opt, "/Fo" + obj))
+            args.extend(extra_postargs)
+
+            try:
+                self.spawn(args)
+            except DistutilsExecError as msg:
+                raise CompileError(msg)
+
+        return objects
+
+    def create_static_lib(
+        self,
+        objects: list[str] | tuple[str, ...],
+        output_libname: str,
+        output_dir: str | None = None,
+        debug: bool = False,
+        target_lang: str | None = None,
+    ) -> None:
+        if not self.initialized:
+            self.initialize()
+        objects, output_dir = self._fix_object_args(objects, output_dir)
+        output_filename = self.library_filename(output_libname, output_dir=output_dir)
+
+        if self._need_link(objects, output_filename):
+            lib_args = objects + ['/OUT:' + output_filename]
+            if debug:
+                pass  # XXX what goes here?
+            try:
+                log.debug('Executing "%s" %s', self.lib, ' '.join(lib_args))
+                self.spawn([self.lib] + lib_args)
+            except DistutilsExecError as msg:
+                raise LibError(msg)
+        else:
+            log.debug("skipping %s (up-to-date)", output_filename)
+
+    def link(
+        self,
+        target_desc: str,
+        objects: list[str] | tuple[str, ...],
+        output_filename: str,
+        output_dir: str | None = None,
+        libraries: list[str] | tuple[str, ...] | None = None,
+        library_dirs: list[str] | tuple[str, ...] | None = None,
+        runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
+        export_symbols: Iterable[str] | None = None,
+        debug: bool = False,
+        extra_preargs: list[str] | None = None,
+        extra_postargs: Iterable[str] | None = None,
+        build_temp: str | os.PathLike[str] | None = None,
+        target_lang: str | None = None,
+    ) -> None:
+        if not self.initialized:
+            self.initialize()
+        objects, output_dir = self._fix_object_args(objects, output_dir)
+        fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)
+        libraries, library_dirs, runtime_library_dirs = fixed_args
+
+        if runtime_library_dirs:
+            self.warn(
+                "I don't know what to do with 'runtime_library_dirs': "
+                + str(runtime_library_dirs)
+            )
+
+        lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)
+        if output_dir is not None:
+            output_filename = os.path.join(output_dir, output_filename)
+
+        if self._need_link(objects, output_filename):
+            ldflags = self._ldflags[target_desc, debug]
+
+            export_opts = ["/EXPORT:" + sym for sym in (export_symbols or [])]
+
+            ld_args = (
+                ldflags + lib_opts + export_opts + objects + ['/OUT:' + output_filename]
+            )
+
+            # The MSVC linker generates .lib and .exp files, which cannot be
+            # suppressed by any linker switches. The .lib files may even be
+            # needed! Make sure they are generated in the temporary build
+            # directory. Since they have different names for debug and release
+            # builds, they can go into the same directory.
+            build_temp = os.path.dirname(objects[0])
+            if export_symbols is not None:
+                (dll_name, dll_ext) = os.path.splitext(
+                    os.path.basename(output_filename)
+                )
+                implib_file = os.path.join(build_temp, self.library_filename(dll_name))
+                ld_args.append('/IMPLIB:' + implib_file)
+
+            if extra_preargs:
+                ld_args[:0] = extra_preargs
+            if extra_postargs:
+                ld_args.extend(extra_postargs)
+
+            output_dir = os.path.dirname(os.path.abspath(output_filename))
+            self.mkpath(output_dir)
+            try:
+                log.debug('Executing "%s" %s', self.linker, ' '.join(ld_args))
+                self.spawn([self.linker] + ld_args)
+            except DistutilsExecError as msg:
+                raise LinkError(msg)
+        else:
+            log.debug("skipping %s (up-to-date)", output_filename)
+
+    def spawn(self, cmd):
+        env = dict(os.environ, PATH=self._paths)
+        with self._fallback_spawn(cmd, env) as fallback:
+            return super().spawn(cmd, env=env)
+        return fallback.value
+
+    @contextlib.contextmanager
+    def _fallback_spawn(self, cmd, env):
+        """
+        Discovered in pypa/distutils#15, some tools monkeypatch the compiler,
+        so the 'env' kwarg causes a TypeError. Detect this condition and
+        restore the legacy, unsafe behavior.
+        """
+        bag = type('Bag', (), {})()
+        try:
+            yield bag
+        except TypeError as exc:
+            if "unexpected keyword argument 'env'" not in str(exc):
+                raise
+        else:
+            return
+        warnings.warn("Fallback spawn triggered. Please update distutils monkeypatch.")
+        with mock.patch.dict('os.environ', env):
+            bag.value = super().spawn(cmd)
+
+    # -- Miscellaneous methods -----------------------------------------
+    # These are all used by the 'gen_lib_options() function, in
+    # ccompiler.py.
+
+    def library_dir_option(self, dir):
+        return "/LIBPATH:" + dir
+
+    def runtime_library_dir_option(self, dir):
+        raise DistutilsPlatformError(
+            "don't know how to set runtime library search path for MSVC"
+        )
+
+    def library_option(self, lib):
+        return self.library_filename(lib)
+
+    def find_library_file(self, dirs, lib, debug=False):
+        # Prefer a debugging library if found (and requested), but deal
+        # with it if we don't have one.
+        if debug:
+            try_names = [lib + "_d", lib]
+        else:
+            try_names = [lib]
+        for dir in dirs:
+            for name in try_names:
+                libfile = os.path.join(dir, self.library_filename(name))
+                if os.path.isfile(libfile):
+                    return libfile
+        else:
+            # Oops, didn't find it in *any* of 'dirs'
+            return None
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_base.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_base.py
new file mode 100644
index 00000000..a762e2b6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_base.py
@@ -0,0 +1,83 @@
+import platform
+import sysconfig
+import textwrap
+
+import pytest
+
+from .. import base
+
+pytestmark = pytest.mark.usefixtures('suppress_path_mangle')
+
+
+@pytest.fixture
+def c_file(tmp_path):
+    c_file = tmp_path / 'foo.c'
+    gen_headers = ('Python.h',)
+    is_windows = platform.system() == "Windows"
+    plat_headers = ('windows.h',) * is_windows
+    all_headers = gen_headers + plat_headers
+    headers = '\n'.join(f'#include <{header}>\n' for header in all_headers)
+    payload = (
+        textwrap.dedent(
+            """
+        #headers
+        void PyInit_foo(void) {}
+        """
+        )
+        .lstrip()
+        .replace('#headers', headers)
+    )
+    c_file.write_text(payload, encoding='utf-8')
+    return c_file
+
+
+def test_set_include_dirs(c_file):
+    """
+    Extensions should build even if set_include_dirs is invoked.
+    In particular, compiler-specific paths should not be overridden.
+    """
+    compiler = base.new_compiler()
+    python = sysconfig.get_paths()['include']
+    compiler.set_include_dirs([python])
+    compiler.compile([c_file])
+
+    # do it again, setting include dirs after any initialization
+    compiler.set_include_dirs([python])
+    compiler.compile([c_file])
+
+
+def test_has_function_prototype():
+    # Issue https://github.com/pypa/setuptools/issues/3648
+    # Test prototype-generating behavior.
+
+    compiler = base.new_compiler()
+
+    # Every C implementation should have these.
+    assert compiler.has_function('abort')
+    assert compiler.has_function('exit')
+    with pytest.deprecated_call(match='includes is deprecated'):
+        # abort() is a valid expression with the <stdlib.h> prototype.
+        assert compiler.has_function('abort', includes=['stdlib.h'])
+    with pytest.deprecated_call(match='includes is deprecated'):
+        # But exit() is not valid with the actual prototype in scope.
+        assert not compiler.has_function('exit', includes=['stdlib.h'])
+    # And setuptools_does_not_exist is not declared or defined at all.
+    assert not compiler.has_function('setuptools_does_not_exist')
+    with pytest.deprecated_call(match='includes is deprecated'):
+        assert not compiler.has_function(
+            'setuptools_does_not_exist', includes=['stdio.h']
+        )
+
+
+def test_include_dirs_after_multiple_compile_calls(c_file):
+    """
+    Calling compile multiple times should not change the include dirs
+    (regression test for setuptools issue #3591).
+    """
+    compiler = base.new_compiler()
+    python = sysconfig.get_paths()['include']
+    compiler.set_include_dirs([python])
+    compiler.compile([c_file])
+    assert compiler.include_dirs == [python]
+    compiler.compile([c_file])
+    assert compiler.include_dirs == [python]
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_cygwin.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_cygwin.py
new file mode 100644
index 00000000..9adf6b8e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_cygwin.py
@@ -0,0 +1,76 @@
+"""Tests for distutils.cygwinccompiler."""
+
+import os
+import sys
+from distutils import sysconfig
+from distutils.tests import support
+
+import pytest
+
+from .. import cygwin
+
+
+@pytest.fixture(autouse=True)
+def stuff(request, monkeypatch, distutils_managed_tempdir):
+    self = request.instance
+    self.python_h = os.path.join(self.mkdtemp(), 'python.h')
+    monkeypatch.setattr(sysconfig, 'get_config_h_filename', self._get_config_h_filename)
+    monkeypatch.setattr(sys, 'version', sys.version)
+
+
+class TestCygwinCCompiler(support.TempdirManager):
+    def _get_config_h_filename(self):
+        return self.python_h
+
+    @pytest.mark.skipif('sys.platform != "cygwin"')
+    @pytest.mark.skipif('not os.path.exists("/usr/lib/libbash.dll.a")')
+    def test_find_library_file(self):
+        from distutils.cygwinccompiler import CygwinCCompiler
+
+        compiler = CygwinCCompiler()
+        link_name = "bash"
+        linkable_file = compiler.find_library_file(["/usr/lib"], link_name)
+        assert linkable_file is not None
+        assert os.path.exists(linkable_file)
+        assert linkable_file == f"/usr/lib/lib{link_name:s}.dll.a"
+
+    @pytest.mark.skipif('sys.platform != "cygwin"')
+    def test_runtime_library_dir_option(self):
+        from distutils.cygwinccompiler import CygwinCCompiler
+
+        compiler = CygwinCCompiler()
+        assert compiler.runtime_library_dir_option('/foo') == []
+
+    def test_check_config_h(self):
+        # check_config_h looks for "GCC" in sys.version first
+        # returns CONFIG_H_OK if found
+        sys.version = (
+            '2.6.1 (r261:67515, Dec  6 2008, 16:42:21) \n[GCC '
+            '4.0.1 (Apple Computer, Inc. build 5370)]'
+        )
+
+        assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_OK
+
+        # then it tries to see if it can find "__GNUC__" in pyconfig.h
+        sys.version = 'something without the *CC word'
+
+        # if the file doesn't exist it returns  CONFIG_H_UNCERTAIN
+        assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_UNCERTAIN
+
+        # if it exists but does not contain __GNUC__, it returns CONFIG_H_NOTOK
+        self.write_file(self.python_h, 'xxx')
+        assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_NOTOK
+
+        # and CONFIG_H_OK if __GNUC__ is found
+        self.write_file(self.python_h, 'xxx __GNUC__ xxx')
+        assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_OK
+
+    def test_get_msvcr(self):
+        assert cygwin.get_msvcr() == []
+
+    @pytest.mark.skipif('sys.platform != "cygwin"')
+    def test_dll_libraries_not_none(self):
+        from distutils.cygwinccompiler import CygwinCCompiler
+
+        compiler = CygwinCCompiler()
+        assert compiler.dll_libraries is not None
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_mingw.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_mingw.py
new file mode 100644
index 00000000..dc45687a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_mingw.py
@@ -0,0 +1,48 @@
+from distutils import sysconfig
+from distutils.errors import DistutilsPlatformError
+from distutils.util import is_mingw, split_quoted
+
+import pytest
+
+from .. import cygwin, errors
+
+
+class TestMinGW32Compiler:
+    @pytest.mark.skipif(not is_mingw(), reason='not on mingw')
+    def test_compiler_type(self):
+        compiler = cygwin.MinGW32Compiler()
+        assert compiler.compiler_type == 'mingw32'
+
+    @pytest.mark.skipif(not is_mingw(), reason='not on mingw')
+    def test_set_executables(self, monkeypatch):
+        monkeypatch.setenv('CC', 'cc')
+        monkeypatch.setenv('CXX', 'c++')
+
+        compiler = cygwin.MinGW32Compiler()
+
+        assert compiler.compiler == split_quoted('cc -O -Wall')
+        assert compiler.compiler_so == split_quoted('cc -shared -O -Wall')
+        assert compiler.compiler_cxx == split_quoted('c++ -O -Wall')
+        assert compiler.linker_exe == split_quoted('cc')
+        assert compiler.linker_so == split_quoted('cc -shared')
+
+    @pytest.mark.skipif(not is_mingw(), reason='not on mingw')
+    def test_runtime_library_dir_option(self):
+        compiler = cygwin.MinGW32Compiler()
+        with pytest.raises(DistutilsPlatformError):
+            compiler.runtime_library_dir_option('/usr/lib')
+
+    @pytest.mark.skipif(not is_mingw(), reason='not on mingw')
+    def test_cygwincc_error(self, monkeypatch):
+        monkeypatch.setattr(cygwin, 'is_cygwincc', lambda _: True)
+
+        with pytest.raises(errors.Error):
+            cygwin.MinGW32Compiler()
+
+    @pytest.mark.skipif('sys.platform == "cygwin"')
+    def test_customize_compiler_with_msvc_python(self):
+        # In case we have an MSVC Python build, but still want to use
+        # MinGW32Compiler, then customize_compiler() shouldn't fail at least.
+        # https://github.com/pypa/setuptools/issues/4456
+        compiler = cygwin.MinGW32Compiler()
+        sysconfig.customize_compiler(compiler)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_msvc.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_msvc.py
new file mode 100644
index 00000000..eca83199
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_msvc.py
@@ -0,0 +1,136 @@
+import os
+import sys
+import sysconfig
+import threading
+import unittest.mock as mock
+from distutils.errors import DistutilsPlatformError
+from distutils.tests import support
+from distutils.util import get_platform
+
+import pytest
+
+from .. import msvc
+
+needs_winreg = pytest.mark.skipif('not hasattr(msvc, "winreg")')
+
+
+class Testmsvccompiler(support.TempdirManager):
+    def test_no_compiler(self, monkeypatch):
+        # makes sure query_vcvarsall raises
+        # a DistutilsPlatformError if the compiler
+        # is not found
+        def _find_vcvarsall(plat_spec):
+            return None, None
+
+        monkeypatch.setattr(msvc, '_find_vcvarsall', _find_vcvarsall)
+
+        with pytest.raises(DistutilsPlatformError):
+            msvc._get_vc_env(
+                'wont find this version',
+            )
+
+    @pytest.mark.skipif(
+        not sysconfig.get_platform().startswith("win"),
+        reason="Only run test for non-mingw Windows platforms",
+    )
+    @pytest.mark.parametrize(
+        "plat_name, expected",
+        [
+            ("win-arm64", "win-arm64"),
+            ("win-amd64", "win-amd64"),
+            (None, get_platform()),
+        ],
+    )
+    def test_cross_platform_compilation_paths(self, monkeypatch, plat_name, expected):
+        """
+        Ensure a specified target platform is passed to _get_vcvars_spec.
+        """
+        compiler = msvc.Compiler()
+
+        def _get_vcvars_spec(host_platform, platform):
+            assert platform == expected
+
+        monkeypatch.setattr(msvc, '_get_vcvars_spec', _get_vcvars_spec)
+        compiler.initialize(plat_name)
+
+    @needs_winreg
+    def test_get_vc_env_unicode(self):
+        test_var = 'ṰḖṤṪ┅ṼẨṜ'
+        test_value = '₃⁴₅'
+
+        # Ensure we don't early exit from _get_vc_env
+        old_distutils_use_sdk = os.environ.pop('DISTUTILS_USE_SDK', None)
+        os.environ[test_var] = test_value
+        try:
+            env = msvc._get_vc_env('x86')
+            assert test_var.lower() in env
+            assert test_value == env[test_var.lower()]
+        finally:
+            os.environ.pop(test_var)
+            if old_distutils_use_sdk:
+                os.environ['DISTUTILS_USE_SDK'] = old_distutils_use_sdk
+
+    @needs_winreg
+    @pytest.mark.parametrize('ver', (2015, 2017))
+    def test_get_vc(self, ver):
+        # This function cannot be mocked, so pass if VC is found
+        # and skip otherwise.
+        lookup = getattr(msvc, f'_find_vc{ver}')
+        expected_version = {2015: 14, 2017: 15}[ver]
+        version, path = lookup()
+        if not version:
+            pytest.skip(f"VS {ver} is not installed")
+        assert version >= expected_version
+        assert os.path.isdir(path)
+
+
+class CheckThread(threading.Thread):
+    exc_info = None
+
+    def run(self):
+        try:
+            super().run()
+        except Exception:
+            self.exc_info = sys.exc_info()
+
+    def __bool__(self):
+        return not self.exc_info
+
+
+class TestSpawn:
+    def test_concurrent_safe(self):
+        """
+        Concurrent calls to spawn should have consistent results.
+        """
+        compiler = msvc.Compiler()
+        compiler._paths = "expected"
+        inner_cmd = 'import os; assert os.environ["PATH"] == "expected"'
+        command = [sys.executable, '-c', inner_cmd]
+
+        threads = [
+            CheckThread(target=compiler.spawn, args=[command]) for n in range(100)
+        ]
+        for thread in threads:
+            thread.start()
+        for thread in threads:
+            thread.join()
+        assert all(threads)
+
+    def test_concurrent_safe_fallback(self):
+        """
+        If CCompiler.spawn has been monkey-patched without support
+        for an env, it should still execute.
+        """
+        from distutils import ccompiler
+
+        compiler = msvc.Compiler()
+        compiler._paths = "expected"
+
+        def CCompiler_spawn(self, cmd):
+            "A spawn without an env argument."
+            assert os.environ["PATH"] == "expected"
+
+        with mock.patch.object(ccompiler.CCompiler, 'spawn', CCompiler_spawn):
+            compiler.spawn(["n/a"])
+
+        assert os.environ.get("PATH") != "expected"
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_unix.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_unix.py
new file mode 100644
index 00000000..f4e28984
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/tests/test_unix.py
@@ -0,0 +1,350 @@
+"""Tests for distutils.unixccompiler."""
+
+import os
+import sys
+import unittest.mock as mock
+from distutils import sysconfig
+from distutils.compat import consolidate_linker_args
+from distutils.errors import DistutilsPlatformError
+from distutils.tests import support
+from distutils.tests.compat.py39 import EnvironmentVarGuard
+from distutils.util import _clear_cached_macosx_ver
+
+import pytest
+
+from .. import unix
+
+
+@pytest.fixture(autouse=True)
+def save_values(monkeypatch):
+    monkeypatch.setattr(sys, 'platform', sys.platform)
+    monkeypatch.setattr(sysconfig, 'get_config_var', sysconfig.get_config_var)
+    monkeypatch.setattr(sysconfig, 'get_config_vars', sysconfig.get_config_vars)
+
+
+@pytest.fixture(autouse=True)
+def compiler_wrapper(request):
+    class CompilerWrapper(unix.Compiler):
+        def rpath_foo(self):
+            return self.runtime_library_dir_option('/foo')
+
+    request.instance.cc = CompilerWrapper()
+
+
+class TestUnixCCompiler(support.TempdirManager):
+    @pytest.mark.skipif('platform.system == "Windows"')
+    def test_runtime_libdir_option(self):  # noqa: C901
+        # Issue #5900; GitHub Issue #37
+        #
+        # Ensure RUNPATH is added to extension modules with RPATH if
+        # GNU ld is used
+
+        # darwin
+        sys.platform = 'darwin'
+        darwin_ver_var = 'MACOSX_DEPLOYMENT_TARGET'
+        darwin_rpath_flag = '-Wl,-rpath,/foo'
+        darwin_lib_flag = '-L/foo'
+
+        # (macOS version from syscfg, macOS version from env var) -> flag
+        # Version value of None generates two tests: as None and as empty string
+        # Expected flag value of None means an mismatch exception is expected
+        darwin_test_cases = [
+            ((None, None), darwin_lib_flag),
+            ((None, '11'), darwin_rpath_flag),
+            (('10', None), darwin_lib_flag),
+            (('10.3', None), darwin_lib_flag),
+            (('10.3.1', None), darwin_lib_flag),
+            (('10.5', None), darwin_rpath_flag),
+            (('10.5.1', None), darwin_rpath_flag),
+            (('10.3', '10.3'), darwin_lib_flag),
+            (('10.3', '10.5'), darwin_rpath_flag),
+            (('10.5', '10.3'), darwin_lib_flag),
+            (('10.5', '11'), darwin_rpath_flag),
+            (('10.4', '10'), None),
+        ]
+
+        def make_darwin_gcv(syscfg_macosx_ver):
+            def gcv(var):
+                if var == darwin_ver_var:
+                    return syscfg_macosx_ver
+                return "xxx"
+
+            return gcv
+
+        def do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag):
+            env = os.environ
+            msg = f"macOS version = (sysconfig={syscfg_macosx_ver!r}, env={env_macosx_ver!r})"
+
+            # Save
+            old_gcv = sysconfig.get_config_var
+            old_env_macosx_ver = env.get(darwin_ver_var)
+
+            # Setup environment
+            _clear_cached_macosx_ver()
+            sysconfig.get_config_var = make_darwin_gcv(syscfg_macosx_ver)
+            if env_macosx_ver is not None:
+                env[darwin_ver_var] = env_macosx_ver
+            elif darwin_ver_var in env:
+                env.pop(darwin_ver_var)
+
+            # Run the test
+            if expected_flag is not None:
+                assert self.cc.rpath_foo() == expected_flag, msg
+            else:
+                with pytest.raises(
+                    DistutilsPlatformError, match=darwin_ver_var + r' mismatch'
+                ):
+                    self.cc.rpath_foo()
+
+            # Restore
+            if old_env_macosx_ver is not None:
+                env[darwin_ver_var] = old_env_macosx_ver
+            elif darwin_ver_var in env:
+                env.pop(darwin_ver_var)
+            sysconfig.get_config_var = old_gcv
+            _clear_cached_macosx_ver()
+
+        for macosx_vers, expected_flag in darwin_test_cases:
+            syscfg_macosx_ver, env_macosx_ver = macosx_vers
+            do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag)
+            # Bonus test cases with None interpreted as empty string
+            if syscfg_macosx_ver is None:
+                do_darwin_test("", env_macosx_ver, expected_flag)
+            if env_macosx_ver is None:
+                do_darwin_test(syscfg_macosx_ver, "", expected_flag)
+            if syscfg_macosx_ver is None and env_macosx_ver is None:
+                do_darwin_test("", "", expected_flag)
+
+        old_gcv = sysconfig.get_config_var
+
+        # hp-ux
+        sys.platform = 'hp-ux'
+
+        def gcv(v):
+            return 'xxx'
+
+        sysconfig.get_config_var = gcv
+        assert self.cc.rpath_foo() == ['+s', '-L/foo']
+
+        def gcv(v):
+            return 'gcc'
+
+        sysconfig.get_config_var = gcv
+        assert self.cc.rpath_foo() == ['-Wl,+s', '-L/foo']
+
+        def gcv(v):
+            return 'g++'
+
+        sysconfig.get_config_var = gcv
+        assert self.cc.rpath_foo() == ['-Wl,+s', '-L/foo']
+
+        sysconfig.get_config_var = old_gcv
+
+        # GCC GNULD
+        sys.platform = 'bar'
+
+        def gcv(v):
+            if v == 'CC':
+                return 'gcc'
+            elif v == 'GNULD':
+                return 'yes'
+
+        sysconfig.get_config_var = gcv
+        assert self.cc.rpath_foo() == consolidate_linker_args([
+            '-Wl,--enable-new-dtags',
+            '-Wl,-rpath,/foo',
+        ])
+
+        def gcv(v):
+            if v == 'CC':
+                return 'gcc -pthread -B /bar'
+            elif v == 'GNULD':
+                return 'yes'
+
+        sysconfig.get_config_var = gcv
+        assert self.cc.rpath_foo() == consolidate_linker_args([
+            '-Wl,--enable-new-dtags',
+            '-Wl,-rpath,/foo',
+        ])
+
+        # GCC non-GNULD
+        sys.platform = 'bar'
+
+        def gcv(v):
+            if v == 'CC':
+                return 'gcc'
+            elif v == 'GNULD':
+                return 'no'
+
+        sysconfig.get_config_var = gcv
+        assert self.cc.rpath_foo() == '-Wl,-R/foo'
+
+        # GCC GNULD with fully qualified configuration prefix
+        # see #7617
+        sys.platform = 'bar'
+
+        def gcv(v):
+            if v == 'CC':
+                return 'x86_64-pc-linux-gnu-gcc-4.4.2'
+            elif v == 'GNULD':
+                return 'yes'
+
+        sysconfig.get_config_var = gcv
+        assert self.cc.rpath_foo() == consolidate_linker_args([
+            '-Wl,--enable-new-dtags',
+            '-Wl,-rpath,/foo',
+        ])
+
+        # non-GCC GNULD
+        sys.platform = 'bar'
+
+        def gcv(v):
+            if v == 'CC':
+                return 'cc'
+            elif v == 'GNULD':
+                return 'yes'
+
+        sysconfig.get_config_var = gcv
+        assert self.cc.rpath_foo() == consolidate_linker_args([
+            '-Wl,--enable-new-dtags',
+            '-Wl,-rpath,/foo',
+        ])
+
+        # non-GCC non-GNULD
+        sys.platform = 'bar'
+
+        def gcv(v):
+            if v == 'CC':
+                return 'cc'
+            elif v == 'GNULD':
+                return 'no'
+
+        sysconfig.get_config_var = gcv
+        assert self.cc.rpath_foo() == '-Wl,-R/foo'
+
+    @pytest.mark.skipif('platform.system == "Windows"')
+    def test_cc_overrides_ldshared(self):
+        # Issue #18080:
+        # ensure that setting CC env variable also changes default linker
+        def gcv(v):
+            if v == 'LDSHARED':
+                return 'gcc-4.2 -bundle -undefined dynamic_lookup '
+            return 'gcc-4.2'
+
+        def gcvs(*args, _orig=sysconfig.get_config_vars):
+            if args:
+                return list(map(sysconfig.get_config_var, args))
+            return _orig()
+
+        sysconfig.get_config_var = gcv
+        sysconfig.get_config_vars = gcvs
+        with EnvironmentVarGuard() as env:
+            env['CC'] = 'my_cc'
+            del env['LDSHARED']
+            sysconfig.customize_compiler(self.cc)
+        assert self.cc.linker_so[0] == 'my_cc'
+
+    @pytest.mark.skipif('platform.system == "Windows"')
+    @pytest.mark.usefixtures('disable_macos_customization')
+    def test_cc_overrides_ldshared_for_cxx_correctly(self):
+        """
+        Ensure that setting CC env variable also changes default linker
+        correctly when building C++ extensions.
+
+        pypa/distutils#126
+        """
+
+        def gcv(v):
+            if v == 'LDSHARED':
+                return 'gcc-4.2 -bundle -undefined dynamic_lookup '
+            elif v == 'LDCXXSHARED':
+                return 'g++-4.2 -bundle -undefined dynamic_lookup '
+            elif v == 'CXX':
+                return 'g++-4.2'
+            elif v == 'CC':
+                return 'gcc-4.2'
+            return ''
+
+        def gcvs(*args, _orig=sysconfig.get_config_vars):
+            if args:
+                return list(map(sysconfig.get_config_var, args))
+            return _orig()
+
+        sysconfig.get_config_var = gcv
+        sysconfig.get_config_vars = gcvs
+        with (
+            mock.patch.object(self.cc, 'spawn', return_value=None) as mock_spawn,
+            mock.patch.object(self.cc, '_need_link', return_value=True),
+            mock.patch.object(self.cc, 'mkpath', return_value=None),
+            EnvironmentVarGuard() as env,
+        ):
+            env['CC'] = 'ccache my_cc'
+            env['CXX'] = 'my_cxx'
+            del env['LDSHARED']
+            sysconfig.customize_compiler(self.cc)
+            assert self.cc.linker_so[0:2] == ['ccache', 'my_cc']
+            self.cc.link(None, [], 'a.out', target_lang='c++')
+            call_args = mock_spawn.call_args[0][0]
+            expected = ['my_cxx', '-bundle', '-undefined', 'dynamic_lookup']
+            assert call_args[:4] == expected
+
+    @pytest.mark.skipif('platform.system == "Windows"')
+    def test_explicit_ldshared(self):
+        # Issue #18080:
+        # ensure that setting CC env variable does not change
+        #   explicit LDSHARED setting for linker
+        def gcv(v):
+            if v == 'LDSHARED':
+                return 'gcc-4.2 -bundle -undefined dynamic_lookup '
+            return 'gcc-4.2'
+
+        def gcvs(*args, _orig=sysconfig.get_config_vars):
+            if args:
+                return list(map(sysconfig.get_config_var, args))
+            return _orig()
+
+        sysconfig.get_config_var = gcv
+        sysconfig.get_config_vars = gcvs
+        with EnvironmentVarGuard() as env:
+            env['CC'] = 'my_cc'
+            env['LDSHARED'] = 'my_ld -bundle -dynamic'
+            sysconfig.customize_compiler(self.cc)
+        assert self.cc.linker_so[0] == 'my_ld'
+
+    def test_has_function(self):
+        # Issue https://github.com/pypa/distutils/issues/64:
+        # ensure that setting output_dir does not raise
+        # FileNotFoundError: [Errno 2] No such file or directory: 'a.out'
+        self.cc.output_dir = 'scratch'
+        os.chdir(self.mkdtemp())
+        self.cc.has_function('abort')
+
+    def test_find_library_file(self, monkeypatch):
+        compiler = unix.Compiler()
+        compiler._library_root = lambda dir: dir
+        monkeypatch.setattr(os.path, 'exists', lambda d: 'existing' in d)
+
+        libname = 'libabc.dylib' if sys.platform != 'cygwin' else 'cygabc.dll'
+        dirs = ('/foo/bar/missing', '/foo/bar/existing')
+        assert (
+            compiler.find_library_file(dirs, 'abc').replace('\\', '/')
+            == f'/foo/bar/existing/{libname}'
+        )
+        assert (
+            compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/')
+            == f'/foo/bar/existing/{libname}'
+        )
+
+        monkeypatch.setattr(
+            os.path,
+            'exists',
+            lambda d: 'existing' in d and '.a' in d and '.dll.a' not in d,
+        )
+        assert (
+            compiler.find_library_file(dirs, 'abc').replace('\\', '/')
+            == '/foo/bar/existing/libabc.a'
+        )
+        assert (
+            compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/')
+            == '/foo/bar/existing/libabc.a'
+        )
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/unix.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/unix.py
new file mode 100644
index 00000000..e8a53d45
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/unix.py
@@ -0,0 +1,423 @@
+"""distutils.unixccompiler
+
+Contains the UnixCCompiler class, a subclass of CCompiler that handles
+the "typical" Unix-style command-line C compiler:
+  * macros defined with -Dname[=value]
+  * macros undefined with -Uname
+  * include search directories specified with -Idir
+  * libraries specified with -lllib
+  * library search directories specified with -Ldir
+  * compile handled by 'cc' (or similar) executable with -c option:
+    compiles .c to .o
+  * link static library handled by 'ar' command (possibly with 'ranlib')
+  * link shared library handled by 'cc -shared'
+"""
+
+from __future__ import annotations
+
+import itertools
+import os
+import re
+import shlex
+import sys
+from collections.abc import Iterable
+
+from ... import sysconfig
+from ..._log import log
+from ..._macos_compat import compiler_fixup
+from ..._modified import newer
+from ...compat import consolidate_linker_args
+from ...errors import DistutilsExecError
+from . import base
+from .base import _Macro, gen_lib_options, gen_preprocess_options
+from .errors import (
+    CompileError,
+    LibError,
+    LinkError,
+)
+
+# XXX Things not currently handled:
+#   * optimization/debug/warning flags; we just use whatever's in Python's
+#     Makefile and live with it.  Is this adequate?  If not, we might
+#     have to have a bunch of subclasses GNUCCompiler, SGICCompiler,
+#     SunCCompiler, and I suspect down that road lies madness.
+#   * even if we don't know a warning flag from an optimization flag,
+#     we need some way for outsiders to feed preprocessor/compiler/linker
+#     flags in to us -- eg. a sysadmin might want to mandate certain flags
+#     via a site config file, or a user might want to set something for
+#     compiling this module distribution only via the setup.py command
+#     line, whatever.  As long as these options come from something on the
+#     current system, they can be as system-dependent as they like, and we
+#     should just happily stuff them into the preprocessor/compiler/linker
+#     options and carry on.
+
+
+def _split_env(cmd):
+    """
+    For macOS, split command into 'env' portion (if any)
+    and the rest of the linker command.
+
+    >>> _split_env(['a', 'b', 'c'])
+    ([], ['a', 'b', 'c'])
+    >>> _split_env(['/usr/bin/env', 'A=3', 'gcc'])
+    (['/usr/bin/env', 'A=3'], ['gcc'])
+    """
+    pivot = 0
+    if os.path.basename(cmd[0]) == "env":
+        pivot = 1
+        while '=' in cmd[pivot]:
+            pivot += 1
+    return cmd[:pivot], cmd[pivot:]
+
+
+def _split_aix(cmd):
+    """
+    AIX platforms prefix the compiler with the ld_so_aix
+    script, so split that from the linker command.
+
+    >>> _split_aix(['a', 'b', 'c'])
+    ([], ['a', 'b', 'c'])
+    >>> _split_aix(['/bin/foo/ld_so_aix', 'gcc'])
+    (['/bin/foo/ld_so_aix'], ['gcc'])
+    """
+    pivot = os.path.basename(cmd[0]) == 'ld_so_aix'
+    return cmd[:pivot], cmd[pivot:]
+
+
+def _linker_params(linker_cmd, compiler_cmd):
+    """
+    The linker command usually begins with the compiler
+    command (possibly multiple elements), followed by zero or more
+    params for shared library building.
+
+    If the LDSHARED env variable overrides the linker command,
+    however, the commands may not match.
+
+    Return the best guess of the linker parameters by stripping
+    the linker command. If the compiler command does not
+    match the linker command, assume the linker command is
+    just the first element.
+
+    >>> _linker_params('gcc foo bar'.split(), ['gcc'])
+    ['foo', 'bar']
+    >>> _linker_params('gcc foo bar'.split(), ['other'])
+    ['foo', 'bar']
+    >>> _linker_params('ccache gcc foo bar'.split(), 'ccache gcc'.split())
+    ['foo', 'bar']
+    >>> _linker_params(['gcc'], ['gcc'])
+    []
+    """
+    c_len = len(compiler_cmd)
+    pivot = c_len if linker_cmd[:c_len] == compiler_cmd else 1
+    return linker_cmd[pivot:]
+
+
+class Compiler(base.Compiler):
+    compiler_type = 'unix'
+
+    # These are used by CCompiler in two places: the constructor sets
+    # instance attributes 'preprocessor', 'compiler', etc. from them, and
+    # 'set_executable()' allows any of these to be set.  The defaults here
+    # are pretty generic; they will probably have to be set by an outsider
+    # (eg. using information discovered by the sysconfig about building
+    # Python extensions).
+    executables = {
+        'preprocessor': None,
+        'compiler': ["cc"],
+        'compiler_so': ["cc"],
+        'compiler_cxx': ["c++"],
+        'compiler_so_cxx': ["c++"],
+        'linker_so': ["cc", "-shared"],
+        'linker_so_cxx': ["c++", "-shared"],
+        'linker_exe': ["cc"],
+        'linker_exe_cxx': ["c++", "-shared"],
+        'archiver': ["ar", "-cr"],
+        'ranlib': None,
+    }
+
+    if sys.platform[:6] == "darwin":
+        executables['ranlib'] = ["ranlib"]
+
+    # Needed for the filename generation methods provided by the base
+    # class, CCompiler.  NB. whoever instantiates/uses a particular
+    # UnixCCompiler instance should set 'shared_lib_ext' -- we set a
+    # reasonable common default here, but it's not necessarily used on all
+    # Unices!
+
+    src_extensions = [".c", ".C", ".cc", ".cxx", ".cpp", ".m"]
+    obj_extension = ".o"
+    static_lib_extension = ".a"
+    shared_lib_extension = ".so"
+    dylib_lib_extension = ".dylib"
+    xcode_stub_lib_extension = ".tbd"
+    static_lib_format = shared_lib_format = dylib_lib_format = "lib%s%s"
+    xcode_stub_lib_format = dylib_lib_format
+    if sys.platform == "cygwin":
+        exe_extension = ".exe"
+        shared_lib_extension = ".dll.a"
+        dylib_lib_extension = ".dll"
+        dylib_lib_format = "cyg%s%s"
+
+    def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs):
+        """Remove standard library path from rpath"""
+        libraries, library_dirs, runtime_library_dirs = super()._fix_lib_args(
+            libraries, library_dirs, runtime_library_dirs
+        )
+        libdir = sysconfig.get_config_var('LIBDIR')
+        if (
+            runtime_library_dirs
+            and libdir.startswith("/usr/lib")
+            and (libdir in runtime_library_dirs)
+        ):
+            runtime_library_dirs.remove(libdir)
+        return libraries, library_dirs, runtime_library_dirs
+
+    def preprocess(
+        self,
+        source: str | os.PathLike[str],
+        output_file: str | os.PathLike[str] | None = None,
+        macros: list[_Macro] | None = None,
+        include_dirs: list[str] | tuple[str, ...] | None = None,
+        extra_preargs: list[str] | None = None,
+        extra_postargs: Iterable[str] | None = None,
+    ):
+        fixed_args = self._fix_compile_args(None, macros, include_dirs)
+        ignore, macros, include_dirs = fixed_args
+        pp_opts = gen_preprocess_options(macros, include_dirs)
+        pp_args = self.preprocessor + pp_opts
+        if output_file:
+            pp_args.extend(['-o', output_file])
+        if extra_preargs:
+            pp_args[:0] = extra_preargs
+        if extra_postargs:
+            pp_args.extend(extra_postargs)
+        pp_args.append(source)
+
+        # reasons to preprocess:
+        # - force is indicated
+        # - output is directed to stdout
+        # - source file is newer than the target
+        preprocess = self.force or output_file is None or newer(source, output_file)
+        if not preprocess:
+            return
+
+        if output_file:
+            self.mkpath(os.path.dirname(output_file))
+
+        try:
+            self.spawn(pp_args)
+        except DistutilsExecError as msg:
+            raise CompileError(msg)
+
+    def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
+        compiler_so = compiler_fixup(self.compiler_so, cc_args + extra_postargs)
+        compiler_so_cxx = compiler_fixup(self.compiler_so_cxx, cc_args + extra_postargs)
+        try:
+            if self.detect_language(src) == 'c++':
+                self.spawn(
+                    compiler_so_cxx + cc_args + [src, '-o', obj] + extra_postargs
+                )
+            else:
+                self.spawn(compiler_so + cc_args + [src, '-o', obj] + extra_postargs)
+        except DistutilsExecError as msg:
+            raise CompileError(msg)
+
+    def create_static_lib(
+        self, objects, output_libname, output_dir=None, debug=False, target_lang=None
+    ):
+        objects, output_dir = self._fix_object_args(objects, output_dir)
+
+        output_filename = self.library_filename(output_libname, output_dir=output_dir)
+
+        if self._need_link(objects, output_filename):
+            self.mkpath(os.path.dirname(output_filename))
+            self.spawn(self.archiver + [output_filename] + objects + self.objects)
+
+            # Not many Unices required ranlib anymore -- SunOS 4.x is, I
+            # think the only major Unix that does.  Maybe we need some
+            # platform intelligence here to skip ranlib if it's not
+            # needed -- or maybe Python's configure script took care of
+            # it for us, hence the check for leading colon.
+            if self.ranlib:
+                try:
+                    self.spawn(self.ranlib + [output_filename])
+                except DistutilsExecError as msg:
+                    raise LibError(msg)
+        else:
+            log.debug("skipping %s (up-to-date)", output_filename)
+
+    def link(
+        self,
+        target_desc,
+        objects: list[str] | tuple[str, ...],
+        output_filename,
+        output_dir: str | None = None,
+        libraries: list[str] | tuple[str, ...] | None = None,
+        library_dirs: list[str] | tuple[str, ...] | None = None,
+        runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
+        export_symbols=None,
+        debug=False,
+        extra_preargs=None,
+        extra_postargs=None,
+        build_temp=None,
+        target_lang=None,
+    ):
+        objects, output_dir = self._fix_object_args(objects, output_dir)
+        fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)
+        libraries, library_dirs, runtime_library_dirs = fixed_args
+
+        lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)
+        if not isinstance(output_dir, (str, type(None))):
+            raise TypeError("'output_dir' must be a string or None")
+        if output_dir is not None:
+            output_filename = os.path.join(output_dir, output_filename)
+
+        if self._need_link(objects, output_filename):
+            ld_args = objects + self.objects + lib_opts + ['-o', output_filename]
+            if debug:
+                ld_args[:0] = ['-g']
+            if extra_preargs:
+                ld_args[:0] = extra_preargs
+            if extra_postargs:
+                ld_args.extend(extra_postargs)
+            self.mkpath(os.path.dirname(output_filename))
+            try:
+                # Select a linker based on context: linker_exe when
+                # building an executable or linker_so (with shared options)
+                # when building a shared library.
+                building_exe = target_desc == base.Compiler.EXECUTABLE
+                linker = (
+                    self.linker_exe
+                    if building_exe
+                    else (
+                        self.linker_so_cxx if target_lang == "c++" else self.linker_so
+                    )
+                )[:]
+
+                if target_lang == "c++" and self.compiler_cxx:
+                    env, linker_ne = _split_env(linker)
+                    aix, linker_na = _split_aix(linker_ne)
+                    _, compiler_cxx_ne = _split_env(self.compiler_cxx)
+                    _, linker_exe_ne = _split_env(self.linker_exe)
+
+                    params = _linker_params(linker_na, linker_exe_ne)
+                    linker = env + aix + compiler_cxx_ne + params
+
+                linker = compiler_fixup(linker, ld_args)
+
+                self.spawn(linker + ld_args)
+            except DistutilsExecError as msg:
+                raise LinkError(msg)
+        else:
+            log.debug("skipping %s (up-to-date)", output_filename)
+
+    # -- Miscellaneous methods -----------------------------------------
+    # These are all used by the 'gen_lib_options() function, in
+    # ccompiler.py.
+
+    def library_dir_option(self, dir):
+        return "-L" + dir
+
+    def _is_gcc(self):
+        cc_var = sysconfig.get_config_var("CC")
+        compiler = os.path.basename(shlex.split(cc_var)[0])
+        return "gcc" in compiler or "g++" in compiler
+
+    def runtime_library_dir_option(self, dir: str) -> str | list[str]:  # type: ignore[override] # Fixed in pypa/distutils#339
+        # XXX Hackish, at the very least.  See Python bug #445902:
+        # https://bugs.python.org/issue445902
+        # Linkers on different platforms need different options to
+        # specify that directories need to be added to the list of
+        # directories searched for dependencies when a dynamic library
+        # is sought.  GCC on GNU systems (Linux, FreeBSD, ...) has to
+        # be told to pass the -R option through to the linker, whereas
+        # other compilers and gcc on other systems just know this.
+        # Other compilers may need something slightly different.  At
+        # this time, there's no way to determine this information from
+        # the configuration data stored in the Python installation, so
+        # we use this hack.
+        if sys.platform[:6] == "darwin":
+            from distutils.util import get_macosx_target_ver, split_version
+
+            macosx_target_ver = get_macosx_target_ver()
+            if macosx_target_ver and split_version(macosx_target_ver) >= [10, 5]:
+                return "-Wl,-rpath," + dir
+            else:  # no support for -rpath on earlier macOS versions
+                return "-L" + dir
+        elif sys.platform[:7] == "freebsd":
+            return "-Wl,-rpath=" + dir
+        elif sys.platform[:5] == "hp-ux":
+            return [
+                "-Wl,+s" if self._is_gcc() else "+s",
+                "-L" + dir,
+            ]
+
+        # For all compilers, `-Wl` is the presumed way to pass a
+        # compiler option to the linker
+        if sysconfig.get_config_var("GNULD") == "yes":
+            return consolidate_linker_args([
+                # Force RUNPATH instead of RPATH
+                "-Wl,--enable-new-dtags",
+                "-Wl,-rpath," + dir,
+            ])
+        else:
+            return "-Wl,-R" + dir
+
+    def library_option(self, lib):
+        return "-l" + lib
+
+    @staticmethod
+    def _library_root(dir):
+        """
+        macOS users can specify an alternate SDK using'-isysroot'.
+        Calculate the SDK root if it is specified.
+
+        Note that, as of Xcode 7, Apple SDKs may contain textual stub
+        libraries with .tbd extensions rather than the normal .dylib
+        shared libraries installed in /.  The Apple compiler tool
+        chain handles this transparently but it can cause problems
+        for programs that are being built with an SDK and searching
+        for specific libraries.  Callers of find_library_file need to
+        keep in mind that the base filename of the returned SDK library
+        file might have a different extension from that of the library
+        file installed on the running system, for example:
+          /Applications/Xcode.app/Contents/Developer/Platforms/
+              MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/
+              usr/lib/libedit.tbd
+        vs
+          /usr/lib/libedit.dylib
+        """
+        cflags = sysconfig.get_config_var('CFLAGS')
+        match = re.search(r'-isysroot\s*(\S+)', cflags)
+
+        apply_root = (
+            sys.platform == 'darwin'
+            and match
+            and (
+                dir.startswith('/System/')
+                or (dir.startswith('/usr/') and not dir.startswith('/usr/local/'))
+            )
+        )
+
+        return os.path.join(match.group(1), dir[1:]) if apply_root else dir
+
+    def find_library_file(self, dirs, lib, debug=False):
+        """
+        Second-guess the linker with not much hard
+        data to go on: GCC seems to prefer the shared library, so
+        assume that *all* Unix C compilers do,
+        ignoring even GCC's "-static" option.
+        """
+        lib_names = (
+            self.library_filename(lib, lib_type=type)
+            for type in 'dylib xcode_stub shared static'.split()
+        )
+
+        roots = map(self._library_root, dirs)
+
+        searched = itertools.starmap(os.path.join, itertools.product(roots, lib_names))
+
+        found = filter(os.path.exists, searched)
+
+        # Return None if it could not be found in any dir.
+        return next(found, None)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/zos.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/zos.py
new file mode 100644
index 00000000..82d017fc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/compilers/C/zos.py
@@ -0,0 +1,230 @@
+"""distutils.zosccompiler
+
+Contains the selection of the c & c++ compilers on z/OS. There are several
+different c compilers on z/OS, all of them are optional, so the correct
+one needs to be chosen based on the users input. This is compatible with
+the following compilers:
+
+IBM C/C++ For Open Enterprise Languages on z/OS 2.0
+IBM Open XL C/C++ 1.1 for z/OS
+IBM XL C/C++ V2.4.1 for z/OS 2.4 and 2.5
+IBM z/OS XL C/C++
+"""
+
+import os
+
+from ... import sysconfig
+from ...errors import DistutilsExecError
+from . import unix
+from .errors import CompileError
+
+_cc_args = {
+    'ibm-openxl': [
+        '-m64',
+        '-fvisibility=default',
+        '-fzos-le-char-mode=ascii',
+        '-fno-short-enums',
+    ],
+    'ibm-xlclang': [
+        '-q64',
+        '-qexportall',
+        '-qascii',
+        '-qstrict',
+        '-qnocsect',
+        '-Wa,asa,goff',
+        '-Wa,xplink',
+        '-qgonumber',
+        '-qenum=int',
+        '-Wc,DLL',
+    ],
+    'ibm-xlc': [
+        '-q64',
+        '-qexportall',
+        '-qascii',
+        '-qstrict',
+        '-qnocsect',
+        '-Wa,asa,goff',
+        '-Wa,xplink',
+        '-qgonumber',
+        '-qenum=int',
+        '-Wc,DLL',
+        '-qlanglvl=extc99',
+    ],
+}
+
+_cxx_args = {
+    'ibm-openxl': [
+        '-m64',
+        '-fvisibility=default',
+        '-fzos-le-char-mode=ascii',
+        '-fno-short-enums',
+    ],
+    'ibm-xlclang': [
+        '-q64',
+        '-qexportall',
+        '-qascii',
+        '-qstrict',
+        '-qnocsect',
+        '-Wa,asa,goff',
+        '-Wa,xplink',
+        '-qgonumber',
+        '-qenum=int',
+        '-Wc,DLL',
+    ],
+    'ibm-xlc': [
+        '-q64',
+        '-qexportall',
+        '-qascii',
+        '-qstrict',
+        '-qnocsect',
+        '-Wa,asa,goff',
+        '-Wa,xplink',
+        '-qgonumber',
+        '-qenum=int',
+        '-Wc,DLL',
+        '-qlanglvl=extended0x',
+    ],
+}
+
+_asm_args = {
+    'ibm-openxl': ['-fasm', '-fno-integrated-as', '-Wa,--ASA', '-Wa,--GOFF'],
+    'ibm-xlclang': [],
+    'ibm-xlc': [],
+}
+
+_ld_args = {
+    'ibm-openxl': [],
+    'ibm-xlclang': ['-Wl,dll', '-q64'],
+    'ibm-xlc': ['-Wl,dll', '-q64'],
+}
+
+
+# Python on z/OS is built with no compiler specific options in it's CFLAGS.
+# But each compiler requires it's own specific options to build successfully,
+# though some of the options are common between them
+class Compiler(unix.Compiler):
+    src_extensions = ['.c', '.C', '.cc', '.cxx', '.cpp', '.m', '.s']
+    _cpp_extensions = ['.cc', '.cpp', '.cxx', '.C']
+    _asm_extensions = ['.s']
+
+    def _get_zos_compiler_name(self):
+        zos_compiler_names = [
+            os.path.basename(binary)
+            for envvar in ('CC', 'CXX', 'LDSHARED')
+            if (binary := os.environ.get(envvar, None))
+        ]
+        if len(zos_compiler_names) == 0:
+            return 'ibm-openxl'
+
+        zos_compilers = {}
+        for compiler in (
+            'ibm-clang',
+            'ibm-clang64',
+            'ibm-clang++',
+            'ibm-clang++64',
+            'clang',
+            'clang++',
+            'clang-14',
+        ):
+            zos_compilers[compiler] = 'ibm-openxl'
+
+        for compiler in ('xlclang', 'xlclang++', 'njsc', 'njsc++'):
+            zos_compilers[compiler] = 'ibm-xlclang'
+
+        for compiler in ('xlc', 'xlC', 'xlc++'):
+            zos_compilers[compiler] = 'ibm-xlc'
+
+        return zos_compilers.get(zos_compiler_names[0], 'ibm-openxl')
+
+    def __init__(self, verbose=False, dry_run=False, force=False):
+        super().__init__(verbose, dry_run, force)
+        self.zos_compiler = self._get_zos_compiler_name()
+        sysconfig.customize_compiler(self)
+
+    def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
+        local_args = []
+        if ext in self._cpp_extensions:
+            compiler = self.compiler_cxx
+            local_args.extend(_cxx_args[self.zos_compiler])
+        elif ext in self._asm_extensions:
+            compiler = self.compiler_so
+            local_args.extend(_cc_args[self.zos_compiler])
+            local_args.extend(_asm_args[self.zos_compiler])
+        else:
+            compiler = self.compiler_so
+            local_args.extend(_cc_args[self.zos_compiler])
+        local_args.extend(cc_args)
+
+        try:
+            self.spawn(compiler + local_args + [src, '-o', obj] + extra_postargs)
+        except DistutilsExecError as msg:
+            raise CompileError(msg)
+
+    def runtime_library_dir_option(self, dir):
+        return '-L' + dir
+
+    def link(
+        self,
+        target_desc,
+        objects,
+        output_filename,
+        output_dir=None,
+        libraries=None,
+        library_dirs=None,
+        runtime_library_dirs=None,
+        export_symbols=None,
+        debug=False,
+        extra_preargs=None,
+        extra_postargs=None,
+        build_temp=None,
+        target_lang=None,
+    ):
+        # For a built module to use functions from cpython, it needs to use Pythons
+        # side deck file. The side deck is located beside the libpython3.xx.so
+        ldversion = sysconfig.get_config_var('LDVERSION')
+        if sysconfig.python_build:
+            side_deck_path = os.path.join(
+                sysconfig.get_config_var('abs_builddir'),
+                f'libpython{ldversion}.x',
+            )
+        else:
+            side_deck_path = os.path.join(
+                sysconfig.get_config_var('installed_base'),
+                sysconfig.get_config_var('platlibdir'),
+                f'libpython{ldversion}.x',
+            )
+
+        if os.path.exists(side_deck_path):
+            if extra_postargs:
+                extra_postargs.append(side_deck_path)
+            else:
+                extra_postargs = [side_deck_path]
+
+        # Check and replace libraries included side deck files
+        if runtime_library_dirs:
+            for dir in runtime_library_dirs:
+                for library in libraries[:]:
+                    library_side_deck = os.path.join(dir, f'{library}.x')
+                    if os.path.exists(library_side_deck):
+                        libraries.remove(library)
+                        extra_postargs.append(library_side_deck)
+                        break
+
+        # Any required ld args for the given compiler
+        extra_postargs.extend(_ld_args[self.zos_compiler])
+
+        super().link(
+            target_desc,
+            objects,
+            output_filename,
+            output_dir,
+            libraries,
+            library_dirs,
+            runtime_library_dirs,
+            export_symbols,
+            debug,
+            extra_preargs,
+            extra_postargs,
+            build_temp,
+            target_lang,
+        )
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/core.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/core.py
new file mode 100644
index 00000000..bd62546b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/core.py
@@ -0,0 +1,289 @@
+"""distutils.core
+
+The only module that needs to be imported to use the Distutils; provides
+the 'setup' function (which is to be called from the setup script).  Also
+indirectly provides the Distribution and Command classes, although they are
+really defined in distutils.dist and distutils.cmd.
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+import tokenize
+from collections.abc import Iterable
+
+from .cmd import Command
+from .debug import DEBUG
+
+# Mainly import these so setup scripts can "from distutils.core import" them.
+from .dist import Distribution
+from .errors import (
+    CCompilerError,
+    DistutilsArgError,
+    DistutilsError,
+    DistutilsSetupError,
+)
+from .extension import Extension
+
+__all__ = ['Distribution', 'Command', 'Extension', 'setup']
+
+# This is a barebones help message generated displayed when the user
+# runs the setup script with no arguments at all.  More useful help
+# is generated with various --help options: global help, list commands,
+# and per-command help.
+USAGE = """\
+usage: %(script)s [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
+   or: %(script)s --help [cmd1 cmd2 ...]
+   or: %(script)s --help-commands
+   or: %(script)s cmd --help
+"""
+
+
+def gen_usage(script_name):
+    script = os.path.basename(script_name)
+    return USAGE % locals()
+
+
+# Some mild magic to control the behaviour of 'setup()' from 'run_setup()'.
+_setup_stop_after = None
+_setup_distribution = None
+
+# Legal keyword arguments for the setup() function
+setup_keywords = (
+    'distclass',
+    'script_name',
+    'script_args',
+    'options',
+    'name',
+    'version',
+    'author',
+    'author_email',
+    'maintainer',
+    'maintainer_email',
+    'url',
+    'license',
+    'description',
+    'long_description',
+    'keywords',
+    'platforms',
+    'classifiers',
+    'download_url',
+    'requires',
+    'provides',
+    'obsoletes',
+)
+
+# Legal keyword arguments for the Extension constructor
+extension_keywords = (
+    'name',
+    'sources',
+    'include_dirs',
+    'define_macros',
+    'undef_macros',
+    'library_dirs',
+    'libraries',
+    'runtime_library_dirs',
+    'extra_objects',
+    'extra_compile_args',
+    'extra_link_args',
+    'swig_opts',
+    'export_symbols',
+    'depends',
+    'language',
+)
+
+
+def setup(**attrs):  # noqa: C901
+    """The gateway to the Distutils: do everything your setup script needs
+    to do, in a highly flexible and user-driven way.  Briefly: create a
+    Distribution instance; find and parse config files; parse the command
+    line; run each Distutils command found there, customized by the options
+    supplied to 'setup()' (as keyword arguments), in config files, and on
+    the command line.
+
+    The Distribution instance might be an instance of a class supplied via
+    the 'distclass' keyword argument to 'setup'; if no such class is
+    supplied, then the Distribution class (in dist.py) is instantiated.
+    All other arguments to 'setup' (except for 'cmdclass') are used to set
+    attributes of the Distribution instance.
+
+    The 'cmdclass' argument, if supplied, is a dictionary mapping command
+    names to command classes.  Each command encountered on the command line
+    will be turned into a command class, which is in turn instantiated; any
+    class found in 'cmdclass' is used in place of the default, which is
+    (for command 'foo_bar') class 'foo_bar' in module
+    'distutils.command.foo_bar'.  The command class must provide a
+    'user_options' attribute which is a list of option specifiers for
+    'distutils.fancy_getopt'.  Any command-line options between the current
+    and the next command are used to set attributes of the current command
+    object.
+
+    When the entire command-line has been successfully parsed, calls the
+    'run()' method on each command object in turn.  This method will be
+    driven entirely by the Distribution object (which each command object
+    has a reference to, thanks to its constructor), and the
+    command-specific options that became attributes of each command
+    object.
+    """
+
+    global _setup_stop_after, _setup_distribution
+
+    # Determine the distribution class -- either caller-supplied or
+    # our Distribution (see below).
+    klass = attrs.get('distclass')
+    if klass:
+        attrs.pop('distclass')
+    else:
+        klass = Distribution
+
+    if 'script_name' not in attrs:
+        attrs['script_name'] = os.path.basename(sys.argv[0])
+    if 'script_args' not in attrs:
+        attrs['script_args'] = sys.argv[1:]
+
+    # Create the Distribution instance, using the remaining arguments
+    # (ie. everything except distclass) to initialize it
+    try:
+        _setup_distribution = dist = klass(attrs)
+    except DistutilsSetupError as msg:
+        if 'name' not in attrs:
+            raise SystemExit(f"error in setup command: {msg}")
+        else:
+            raise SystemExit("error in {} setup command: {}".format(attrs['name'], msg))
+
+    if _setup_stop_after == "init":
+        return dist
+
+    # Find and parse the config file(s): they will override options from
+    # the setup script, but be overridden by the command line.
+    dist.parse_config_files()
+
+    if DEBUG:
+        print("options (after parsing config files):")
+        dist.dump_option_dicts()
+
+    if _setup_stop_after == "config":
+        return dist
+
+    # Parse the command line and override config files; any
+    # command-line errors are the end user's fault, so turn them into
+    # SystemExit to suppress tracebacks.
+    try:
+        ok = dist.parse_command_line()
+    except DistutilsArgError as msg:
+        raise SystemExit(gen_usage(dist.script_name) + f"\nerror: {msg}")
+
+    if DEBUG:
+        print("options (after parsing command line):")
+        dist.dump_option_dicts()
+
+    if _setup_stop_after == "commandline":
+        return dist
+
+    # And finally, run all the commands found on the command line.
+    if ok:
+        return run_commands(dist)
+
+    return dist
+
+
+# setup ()
+
+
+def run_commands(dist):
+    """Given a Distribution object run all the commands,
+    raising ``SystemExit`` errors in the case of failure.
+
+    This function assumes that either ``sys.argv`` or ``dist.script_args``
+    is already set accordingly.
+    """
+    try:
+        dist.run_commands()
+    except KeyboardInterrupt:
+        raise SystemExit("interrupted")
+    except OSError as exc:
+        if DEBUG:
+            sys.stderr.write(f"error: {exc}\n")
+            raise
+        else:
+            raise SystemExit(f"error: {exc}")
+
+    except (DistutilsError, CCompilerError) as msg:
+        if DEBUG:
+            raise
+        else:
+            raise SystemExit("error: " + str(msg))
+
+    return dist
+
+
+def run_setup(script_name, script_args: Iterable[str] | None = None, stop_after="run"):
+    """Run a setup script in a somewhat controlled environment, and
+    return the Distribution instance that drives things.  This is useful
+    if you need to find out the distribution meta-data (passed as
+    keyword args from 'script' to 'setup()', or the contents of the
+    config files or command-line.
+
+    'script_name' is a file that will be read and run with 'exec()';
+    'sys.argv[0]' will be replaced with 'script' for the duration of the
+    call.  'script_args' is a list of strings; if supplied,
+    'sys.argv[1:]' will be replaced by 'script_args' for the duration of
+    the call.
+
+    'stop_after' tells 'setup()' when to stop processing; possible
+    values:
+      init
+        stop after the Distribution instance has been created and
+        populated with the keyword arguments to 'setup()'
+      config
+        stop after config files have been parsed (and their data
+        stored in the Distribution instance)
+      commandline
+        stop after the command-line ('sys.argv[1:]' or 'script_args')
+        have been parsed (and the data stored in the Distribution)
+      run [default]
+        stop after all commands have been run (the same as if 'setup()'
+        had been called in the usual way
+
+    Returns the Distribution instance, which provides all information
+    used to drive the Distutils.
+    """
+    if stop_after not in ('init', 'config', 'commandline', 'run'):
+        raise ValueError(f"invalid value for 'stop_after': {stop_after!r}")
+
+    global _setup_stop_after, _setup_distribution
+    _setup_stop_after = stop_after
+
+    save_argv = sys.argv.copy()
+    g = {'__file__': script_name, '__name__': '__main__'}
+    try:
+        try:
+            sys.argv[0] = script_name
+            if script_args is not None:
+                sys.argv[1:] = script_args
+            # tokenize.open supports automatic encoding detection
+            with tokenize.open(script_name) as f:
+                code = f.read().replace(r'\r\n', r'\n')
+                exec(code, g)
+        finally:
+            sys.argv = save_argv
+            _setup_stop_after = None
+    except SystemExit:
+        # Hmm, should we do something if exiting with a non-zero code
+        # (ie. error)?
+        pass
+
+    if _setup_distribution is None:
+        raise RuntimeError(
+            "'distutils.core.setup()' was never called -- "
+            f"perhaps '{script_name}' is not a Distutils setup script?"
+        )
+
+    # I wonder if the setup script's namespace -- g and l -- would be of
+    # any interest to callers?
+    # print "_setup_distribution:", _setup_distribution
+    return _setup_distribution
+
+
+# run_setup ()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/cygwinccompiler.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/cygwinccompiler.py
new file mode 100644
index 00000000..de89e3cd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/cygwinccompiler.py
@@ -0,0 +1,31 @@
+from .compilers.C import cygwin
+from .compilers.C.cygwin import (
+    CONFIG_H_NOTOK,
+    CONFIG_H_OK,
+    CONFIG_H_UNCERTAIN,
+    check_config_h,
+    get_msvcr,
+    is_cygwincc,
+)
+
+__all__ = [
+    'CONFIG_H_NOTOK',
+    'CONFIG_H_OK',
+    'CONFIG_H_UNCERTAIN',
+    'CygwinCCompiler',
+    'Mingw32CCompiler',
+    'check_config_h',
+    'get_msvcr',
+    'is_cygwincc',
+]
+
+
+CygwinCCompiler = cygwin.Compiler
+Mingw32CCompiler = cygwin.MinGW32Compiler
+
+
+get_versions = None
+"""
+A stand-in for the previous get_versions() function to prevent failures
+when monkeypatched. See pypa/setuptools#2969.
+"""
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/debug.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/debug.py
new file mode 100644
index 00000000..daf1660f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/debug.py
@@ -0,0 +1,5 @@
+import os
+
+# If DISTUTILS_DEBUG is anything other than the empty string, we run in
+# debug mode.
+DEBUG = os.environ.get('DISTUTILS_DEBUG')
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/dep_util.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/dep_util.py
new file mode 100644
index 00000000..09a8a2e1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/dep_util.py
@@ -0,0 +1,14 @@
+import warnings
+
+from . import _modified
+
+
+def __getattr__(name):
+    if name not in ['newer', 'newer_group', 'newer_pairwise']:
+        raise AttributeError(name)
+    warnings.warn(
+        "dep_util is Deprecated. Use functions from setuptools instead.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    return getattr(_modified, name)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/dir_util.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/dir_util.py
new file mode 100644
index 00000000..d9782602
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/dir_util.py
@@ -0,0 +1,244 @@
+"""distutils.dir_util
+
+Utility functions for manipulating directories and directory trees."""
+
+import functools
+import itertools
+import os
+import pathlib
+
+from . import file_util
+from ._log import log
+from .errors import DistutilsFileError, DistutilsInternalError
+
+
+class SkipRepeatAbsolutePaths(set):
+    """
+    Cache for mkpath.
+
+    In addition to cheapening redundant calls, eliminates redundant
+    "creating /foo/bar/baz" messages in dry-run mode.
+    """
+
+    def __init__(self):
+        SkipRepeatAbsolutePaths.instance = self
+
+    @classmethod
+    def clear(cls):
+        super(cls, cls.instance).clear()
+
+    def wrap(self, func):
+        @functools.wraps(func)
+        def wrapper(path, *args, **kwargs):
+            if path.absolute() in self:
+                return
+            result = func(path, *args, **kwargs)
+            self.add(path.absolute())
+            return result
+
+        return wrapper
+
+
+# Python 3.8 compatibility
+wrapper = SkipRepeatAbsolutePaths().wrap
+
+
+@functools.singledispatch
+@wrapper
+def mkpath(name: pathlib.Path, mode=0o777, verbose=True, dry_run=False) -> None:
+    """Create a directory and any missing ancestor directories.
+
+    If the directory already exists (or if 'name' is the empty string, which
+    means the current directory, which of course exists), then do nothing.
+    Raise DistutilsFileError if unable to create some directory along the way
+    (eg. some sub-path exists, but is a file rather than a directory).
+    If 'verbose' is true, log the directory created.
+    """
+    if verbose and not name.is_dir():
+        log.info("creating %s", name)
+
+    try:
+        dry_run or name.mkdir(mode=mode, parents=True, exist_ok=True)
+    except OSError as exc:
+        raise DistutilsFileError(f"could not create '{name}': {exc.args[-1]}")
+
+
+@mkpath.register
+def _(name: str, *args, **kwargs):
+    return mkpath(pathlib.Path(name), *args, **kwargs)
+
+
+@mkpath.register
+def _(name: None, *args, **kwargs):
+    """
+    Detect a common bug -- name is None.
+    """
+    raise DistutilsInternalError(f"mkpath: 'name' must be a string (got {name!r})")
+
+
+def create_tree(base_dir, files, mode=0o777, verbose=True, dry_run=False):
+    """Create all the empty directories under 'base_dir' needed to put 'files'
+    there.
+
+    'base_dir' is just the name of a directory which doesn't necessarily
+    exist yet; 'files' is a list of filenames to be interpreted relative to
+    'base_dir'.  'base_dir' + the directory portion of every file in 'files'
+    will be created if it doesn't already exist.  'mode', 'verbose' and
+    'dry_run' flags are as for 'mkpath()'.
+    """
+    # First get the list of directories to create
+    need_dir = set(os.path.join(base_dir, os.path.dirname(file)) for file in files)
+
+    # Now create them
+    for dir in sorted(need_dir):
+        mkpath(dir, mode, verbose=verbose, dry_run=dry_run)
+
+
+def copy_tree(
+    src,
+    dst,
+    preserve_mode=True,
+    preserve_times=True,
+    preserve_symlinks=False,
+    update=False,
+    verbose=True,
+    dry_run=False,
+):
+    """Copy an entire directory tree 'src' to a new location 'dst'.
+
+    Both 'src' and 'dst' must be directory names.  If 'src' is not a
+    directory, raise DistutilsFileError.  If 'dst' does not exist, it is
+    created with 'mkpath()'.  The end result of the copy is that every
+    file in 'src' is copied to 'dst', and directories under 'src' are
+    recursively copied to 'dst'.  Return the list of files that were
+    copied or might have been copied, using their output name.  The
+    return value is unaffected by 'update' or 'dry_run': it is simply
+    the list of all files under 'src', with the names changed to be
+    under 'dst'.
+
+    'preserve_mode' and 'preserve_times' are the same as for
+    'copy_file'; note that they only apply to regular files, not to
+    directories.  If 'preserve_symlinks' is true, symlinks will be
+    copied as symlinks (on platforms that support them!); otherwise
+    (the default), the destination of the symlink will be copied.
+    'update' and 'verbose' are the same as for 'copy_file'.
+    """
+    if not dry_run and not os.path.isdir(src):
+        raise DistutilsFileError(f"cannot copy tree '{src}': not a directory")
+    try:
+        names = os.listdir(src)
+    except OSError as e:
+        if dry_run:
+            names = []
+        else:
+            raise DistutilsFileError(f"error listing files in '{src}': {e.strerror}")
+
+    if not dry_run:
+        mkpath(dst, verbose=verbose)
+
+    copy_one = functools.partial(
+        _copy_one,
+        src=src,
+        dst=dst,
+        preserve_symlinks=preserve_symlinks,
+        verbose=verbose,
+        dry_run=dry_run,
+        preserve_mode=preserve_mode,
+        preserve_times=preserve_times,
+        update=update,
+    )
+    return list(itertools.chain.from_iterable(map(copy_one, names)))
+
+
+def _copy_one(
+    name,
+    *,
+    src,
+    dst,
+    preserve_symlinks,
+    verbose,
+    dry_run,
+    preserve_mode,
+    preserve_times,
+    update,
+):
+    src_name = os.path.join(src, name)
+    dst_name = os.path.join(dst, name)
+
+    if name.startswith('.nfs'):
+        # skip NFS rename files
+        return
+
+    if preserve_symlinks and os.path.islink(src_name):
+        link_dest = os.readlink(src_name)
+        if verbose >= 1:
+            log.info("linking %s -> %s", dst_name, link_dest)
+        if not dry_run:
+            os.symlink(link_dest, dst_name)
+        yield dst_name
+
+    elif os.path.isdir(src_name):
+        yield from copy_tree(
+            src_name,
+            dst_name,
+            preserve_mode,
+            preserve_times,
+            preserve_symlinks,
+            update,
+            verbose=verbose,
+            dry_run=dry_run,
+        )
+    else:
+        file_util.copy_file(
+            src_name,
+            dst_name,
+            preserve_mode,
+            preserve_times,
+            update,
+            verbose=verbose,
+            dry_run=dry_run,
+        )
+        yield dst_name
+
+
+def _build_cmdtuple(path, cmdtuples):
+    """Helper for remove_tree()."""
+    for f in os.listdir(path):
+        real_f = os.path.join(path, f)
+        if os.path.isdir(real_f) and not os.path.islink(real_f):
+            _build_cmdtuple(real_f, cmdtuples)
+        else:
+            cmdtuples.append((os.remove, real_f))
+    cmdtuples.append((os.rmdir, path))
+
+
+def remove_tree(directory, verbose=True, dry_run=False):
+    """Recursively remove an entire directory tree.
+
+    Any errors are ignored (apart from being reported to stdout if 'verbose'
+    is true).
+    """
+    if verbose >= 1:
+        log.info("removing '%s' (and everything under it)", directory)
+    if dry_run:
+        return
+    cmdtuples = []
+    _build_cmdtuple(directory, cmdtuples)
+    for cmd in cmdtuples:
+        try:
+            cmd[0](cmd[1])
+            # Clear the cache
+            SkipRepeatAbsolutePaths.clear()
+        except OSError as exc:
+            log.warning("error removing %s: %s", directory, exc)
+
+
+def ensure_relative(path):
+    """Take the full path 'path', and make it a relative path.
+
+    This is useful to make 'path' the second argument to os.path.join().
+    """
+    drive, path = os.path.splitdrive(path)
+    if path[0:1] == os.sep:
+        path = drive + path[1:]
+    return path
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/dist.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/dist.py
new file mode 100644
index 00000000..37b788df
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/dist.py
@@ -0,0 +1,1386 @@
+"""distutils.dist
+
+Provides the Distribution class, which represents the module distribution
+being built/installed/distributed.
+"""
+
+from __future__ import annotations
+
+import contextlib
+import logging
+import os
+import pathlib
+import re
+import sys
+import warnings
+from collections.abc import Iterable, MutableMapping
+from email import message_from_file
+from typing import (
+    IO,
+    TYPE_CHECKING,
+    Any,
+    ClassVar,
+    Literal,
+    TypeVar,
+    Union,
+    overload,
+)
+
+from packaging.utils import canonicalize_name, canonicalize_version
+
+from ._log import log
+from .debug import DEBUG
+from .errors import (
+    DistutilsArgError,
+    DistutilsClassError,
+    DistutilsModuleError,
+    DistutilsOptionError,
+)
+from .fancy_getopt import FancyGetopt, translate_longopt
+from .util import check_environ, rfc822_escape, strtobool
+
+if TYPE_CHECKING:
+    from _typeshed import SupportsWrite
+    from typing_extensions import TypeAlias
+
+    # type-only import because of mutual dependence between these modules
+    from .cmd import Command
+
+_CommandT = TypeVar("_CommandT", bound="Command")
+_OptionsList: TypeAlias = list[
+    Union[tuple[str, Union[str, None], str, int], tuple[str, Union[str, None], str]]
+]
+
+
+# Regex to define acceptable Distutils command names.  This is not *quite*
+# the same as a Python NAME -- I don't allow leading underscores.  The fact
+# that they're very similar is no coincidence; the default naming scheme is
+# to look for a Python module named after the command.
+command_re = re.compile(r'^[a-zA-Z]([a-zA-Z0-9_]*)$')
+
+
+def _ensure_list(value: str | Iterable[str], fieldname) -> str | list[str]:
+    if isinstance(value, str):
+        # a string containing comma separated values is okay.  It will
+        # be converted to a list by Distribution.finalize_options().
+        pass
+    elif not isinstance(value, list):
+        # passing a tuple or an iterator perhaps, warn and convert
+        typename = type(value).__name__
+        msg = "Warning: '{fieldname}' should be a list, got type '{typename}'"
+        msg = msg.format(**locals())
+        log.warning(msg)
+        value = list(value)
+    return value
+
+
+class Distribution:
+    """The core of the Distutils.  Most of the work hiding behind 'setup'
+    is really done within a Distribution instance, which farms the work out
+    to the Distutils commands specified on the command line.
+
+    Setup scripts will almost never instantiate Distribution directly,
+    unless the 'setup()' function is totally inadequate to their needs.
+    However, it is conceivable that a setup script might wish to subclass
+    Distribution for some specialized purpose, and then pass the subclass
+    to 'setup()' as the 'distclass' keyword argument.  If so, it is
+    necessary to respect the expectations that 'setup' has of Distribution.
+    See the code for 'setup()', in core.py, for details.
+    """
+
+    # 'global_options' describes the command-line options that may be
+    # supplied to the setup script prior to any actual commands.
+    # Eg. "./setup.py -n" or "./setup.py --quiet" both take advantage of
+    # these global options.  This list should be kept to a bare minimum,
+    # since every global option is also valid as a command option -- and we
+    # don't want to pollute the commands with too many options that they
+    # have minimal control over.
+    # The fourth entry for verbose means that it can be repeated.
+    global_options: ClassVar[_OptionsList] = [
+        ('verbose', 'v', "run verbosely (default)", 1),
+        ('quiet', 'q', "run quietly (turns verbosity off)"),
+        ('dry-run', 'n', "don't actually do anything"),
+        ('help', 'h', "show detailed help message"),
+        ('no-user-cfg', None, 'ignore pydistutils.cfg in your home directory'),
+    ]
+
+    # 'common_usage' is a short (2-3 line) string describing the common
+    # usage of the setup script.
+    common_usage: ClassVar[str] = """\
+Common commands: (see '--help-commands' for more)
+
+  setup.py build      will build the package underneath 'build/'
+  setup.py install    will install the package
+"""
+
+    # options that are not propagated to the commands
+    display_options: ClassVar[_OptionsList] = [
+        ('help-commands', None, "list all available commands"),
+        ('name', None, "print package name"),
+        ('version', 'V', "print package version"),
+        ('fullname', None, "print <package name>-<version>"),
+        ('author', None, "print the author's name"),
+        ('author-email', None, "print the author's email address"),
+        ('maintainer', None, "print the maintainer's name"),
+        ('maintainer-email', None, "print the maintainer's email address"),
+        ('contact', None, "print the maintainer's name if known, else the author's"),
+        (
+            'contact-email',
+            None,
+            "print the maintainer's email address if known, else the author's",
+        ),
+        ('url', None, "print the URL for this package"),
+        ('license', None, "print the license of the package"),
+        ('licence', None, "alias for --license"),
+        ('description', None, "print the package description"),
+        ('long-description', None, "print the long package description"),
+        ('platforms', None, "print the list of platforms"),
+        ('classifiers', None, "print the list of classifiers"),
+        ('keywords', None, "print the list of keywords"),
+        ('provides', None, "print the list of packages/modules provided"),
+        ('requires', None, "print the list of packages/modules required"),
+        ('obsoletes', None, "print the list of packages/modules made obsolete"),
+    ]
+    display_option_names: ClassVar[list[str]] = [
+        translate_longopt(x[0]) for x in display_options
+    ]
+
+    # negative options are options that exclude other options
+    negative_opt: ClassVar[dict[str, str]] = {'quiet': 'verbose'}
+
+    # -- Creation/initialization methods -------------------------------
+
+    # Can't Unpack a TypedDict with optional properties, so using Any instead
+    def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None:  # noqa: C901
+        """Construct a new Distribution instance: initialize all the
+        attributes of a Distribution, and then use 'attrs' (a dictionary
+        mapping attribute names to values) to assign some of those
+        attributes their "real" values.  (Any attributes not mentioned in
+        'attrs' will be assigned to some null value: 0, None, an empty list
+        or dictionary, etc.)  Most importantly, initialize the
+        'command_obj' attribute to the empty dictionary; this will be
+        filled in with real command objects by 'parse_command_line()'.
+        """
+
+        # Default values for our command-line options
+        self.verbose = True
+        self.dry_run = False
+        self.help = False
+        for attr in self.display_option_names:
+            setattr(self, attr, False)
+
+        # Store the distribution meta-data (name, version, author, and so
+        # forth) in a separate object -- we're getting to have enough
+        # information here (and enough command-line options) that it's
+        # worth it.  Also delegate 'get_XXX()' methods to the 'metadata'
+        # object in a sneaky and underhanded (but efficient!) way.
+        self.metadata = DistributionMetadata()
+        for basename in self.metadata._METHOD_BASENAMES:
+            method_name = "get_" + basename
+            setattr(self, method_name, getattr(self.metadata, method_name))
+
+        # 'cmdclass' maps command names to class objects, so we
+        # can 1) quickly figure out which class to instantiate when
+        # we need to create a new command object, and 2) have a way
+        # for the setup script to override command classes
+        self.cmdclass: dict[str, type[Command]] = {}
+
+        # 'command_packages' is a list of packages in which commands
+        # are searched for.  The factory for command 'foo' is expected
+        # to be named 'foo' in the module 'foo' in one of the packages
+        # named here.  This list is searched from the left; an error
+        # is raised if no named package provides the command being
+        # searched for.  (Always access using get_command_packages().)
+        self.command_packages: str | list[str] | None = None
+
+        # 'script_name' and 'script_args' are usually set to sys.argv[0]
+        # and sys.argv[1:], but they can be overridden when the caller is
+        # not necessarily a setup script run from the command-line.
+        self.script_name: str | os.PathLike[str] | None = None
+        self.script_args: list[str] | None = None
+
+        # 'command_options' is where we store command options between
+        # parsing them (from config files, the command-line, etc.) and when
+        # they are actually needed -- ie. when the command in question is
+        # instantiated.  It is a dictionary of dictionaries of 2-tuples:
+        #   command_options = { command_name : { option : (source, value) } }
+        self.command_options: dict[str, dict[str, tuple[str, str]]] = {}
+
+        # 'dist_files' is the list of (command, pyversion, file) that
+        # have been created by any dist commands run so far. This is
+        # filled regardless of whether the run is dry or not. pyversion
+        # gives sysconfig.get_python_version() if the dist file is
+        # specific to a Python version, 'any' if it is good for all
+        # Python versions on the target platform, and '' for a source
+        # file. pyversion should not be used to specify minimum or
+        # maximum required Python versions; use the metainfo for that
+        # instead.
+        self.dist_files: list[tuple[str, str, str]] = []
+
+        # These options are really the business of various commands, rather
+        # than of the Distribution itself.  We provide aliases for them in
+        # Distribution as a convenience to the developer.
+        self.packages = None
+        self.package_data: dict[str, list[str]] = {}
+        self.package_dir = None
+        self.py_modules = None
+        self.libraries = None
+        self.headers = None
+        self.ext_modules = None
+        self.ext_package = None
+        self.include_dirs = None
+        self.extra_path = None
+        self.scripts = None
+        self.data_files = None
+        self.password = ''
+
+        # And now initialize bookkeeping stuff that can't be supplied by
+        # the caller at all.  'command_obj' maps command names to
+        # Command instances -- that's how we enforce that every command
+        # class is a singleton.
+        self.command_obj: dict[str, Command] = {}
+
+        # 'have_run' maps command names to boolean values; it keeps track
+        # of whether we have actually run a particular command, to make it
+        # cheap to "run" a command whenever we think we might need to -- if
+        # it's already been done, no need for expensive filesystem
+        # operations, we just check the 'have_run' dictionary and carry on.
+        # It's only safe to query 'have_run' for a command class that has
+        # been instantiated -- a false value will be inserted when the
+        # command object is created, and replaced with a true value when
+        # the command is successfully run.  Thus it's probably best to use
+        # '.get()' rather than a straight lookup.
+        self.have_run: dict[str, bool] = {}
+
+        # Now we'll use the attrs dictionary (ultimately, keyword args from
+        # the setup script) to possibly override any or all of these
+        # distribution options.
+
+        if attrs:
+            # Pull out the set of command options and work on them
+            # specifically.  Note that this order guarantees that aliased
+            # command options will override any supplied redundantly
+            # through the general options dictionary.
+            options = attrs.get('options')
+            if options is not None:
+                del attrs['options']
+                for command, cmd_options in options.items():
+                    opt_dict = self.get_option_dict(command)
+                    for opt, val in cmd_options.items():
+                        opt_dict[opt] = ("setup script", val)
+
+            if 'licence' in attrs:
+                attrs['license'] = attrs['licence']
+                del attrs['licence']
+                msg = "'licence' distribution option is deprecated; use 'license'"
+                warnings.warn(msg)
+
+            # Now work on the rest of the attributes.  Any attribute that's
+            # not already defined is invalid!
+            for key, val in attrs.items():
+                if hasattr(self.metadata, "set_" + key):
+                    getattr(self.metadata, "set_" + key)(val)
+                elif hasattr(self.metadata, key):
+                    setattr(self.metadata, key, val)
+                elif hasattr(self, key):
+                    setattr(self, key, val)
+                else:
+                    msg = f"Unknown distribution option: {key!r}"
+                    warnings.warn(msg)
+
+        # no-user-cfg is handled before other command line args
+        # because other args override the config files, and this
+        # one is needed before we can load the config files.
+        # If attrs['script_args'] wasn't passed, assume false.
+        #
+        # This also make sure we just look at the global options
+        self.want_user_cfg = True
+
+        if self.script_args is not None:
+            # Coerce any possible iterable from attrs into a list
+            self.script_args = list(self.script_args)
+            for arg in self.script_args:
+                if not arg.startswith('-'):
+                    break
+                if arg == '--no-user-cfg':
+                    self.want_user_cfg = False
+                    break
+
+        self.finalize_options()
+
+    def get_option_dict(self, command):
+        """Get the option dictionary for a given command.  If that
+        command's option dictionary hasn't been created yet, then create it
+        and return the new dictionary; otherwise, return the existing
+        option dictionary.
+        """
+        dict = self.command_options.get(command)
+        if dict is None:
+            dict = self.command_options[command] = {}
+        return dict
+
+    def dump_option_dicts(self, header=None, commands=None, indent: str = "") -> None:
+        from pprint import pformat
+
+        if commands is None:  # dump all command option dicts
+            commands = sorted(self.command_options.keys())
+
+        if header is not None:
+            self.announce(indent + header)
+            indent = indent + "  "
+
+        if not commands:
+            self.announce(indent + "no commands known yet")
+            return
+
+        for cmd_name in commands:
+            opt_dict = self.command_options.get(cmd_name)
+            if opt_dict is None:
+                self.announce(indent + f"no option dict for '{cmd_name}' command")
+            else:
+                self.announce(indent + f"option dict for '{cmd_name}' command:")
+                out = pformat(opt_dict)
+                for line in out.split('\n'):
+                    self.announce(indent + "  " + line)
+
+    # -- Config file finding/parsing methods ---------------------------
+
+    def find_config_files(self):
+        """Find as many configuration files as should be processed for this
+        platform, and return a list of filenames in the order in which they
+        should be parsed.  The filenames returned are guaranteed to exist
+        (modulo nasty race conditions).
+
+        There are multiple possible config files:
+        - distutils.cfg in the Distutils installation directory (i.e.
+          where the top-level Distutils __inst__.py file lives)
+        - a file in the user's home directory named .pydistutils.cfg
+          on Unix and pydistutils.cfg on Windows/Mac; may be disabled
+          with the ``--no-user-cfg`` option
+        - setup.cfg in the current directory
+        - a file named by an environment variable
+        """
+        check_environ()
+        files = [str(path) for path in self._gen_paths() if os.path.isfile(path)]
+
+        if DEBUG:
+            self.announce("using config files: {}".format(', '.join(files)))
+
+        return files
+
+    def _gen_paths(self):
+        # The system-wide Distutils config file
+        sys_dir = pathlib.Path(sys.modules['distutils'].__file__).parent
+        yield sys_dir / "distutils.cfg"
+
+        # The per-user config file
+        prefix = '.' * (os.name == 'posix')
+        filename = prefix + 'pydistutils.cfg'
+        if self.want_user_cfg:
+            with contextlib.suppress(RuntimeError):
+                yield pathlib.Path('~').expanduser() / filename
+
+        # All platforms support local setup.cfg
+        yield pathlib.Path('setup.cfg')
+
+        # Additional config indicated in the environment
+        with contextlib.suppress(TypeError):
+            yield pathlib.Path(os.getenv("DIST_EXTRA_CONFIG"))
+
+    def parse_config_files(self, filenames=None):  # noqa: C901
+        from configparser import ConfigParser
+
+        # Ignore install directory options if we have a venv
+        if sys.prefix != sys.base_prefix:
+            ignore_options = [
+                'install-base',
+                'install-platbase',
+                'install-lib',
+                'install-platlib',
+                'install-purelib',
+                'install-headers',
+                'install-scripts',
+                'install-data',
+                'prefix',
+                'exec-prefix',
+                'home',
+                'user',
+                'root',
+            ]
+        else:
+            ignore_options = []
+
+        ignore_options = frozenset(ignore_options)
+
+        if filenames is None:
+            filenames = self.find_config_files()
+
+        if DEBUG:
+            self.announce("Distribution.parse_config_files():")
+
+        parser = ConfigParser()
+        for filename in filenames:
+            if DEBUG:
+                self.announce(f"  reading {filename}")
+            parser.read(filename, encoding='utf-8')
+            for section in parser.sections():
+                options = parser.options(section)
+                opt_dict = self.get_option_dict(section)
+
+                for opt in options:
+                    if opt != '__name__' and opt not in ignore_options:
+                        val = parser.get(section, opt)
+                        opt = opt.replace('-', '_')
+                        opt_dict[opt] = (filename, val)
+
+            # Make the ConfigParser forget everything (so we retain
+            # the original filenames that options come from)
+            parser.__init__()
+
+        # If there was a "global" section in the config file, use it
+        # to set Distribution options.
+
+        if 'global' in self.command_options:
+            for opt, (_src, val) in self.command_options['global'].items():
+                alias = self.negative_opt.get(opt)
+                try:
+                    if alias:
+                        setattr(self, alias, not strtobool(val))
+                    elif opt in ('verbose', 'dry_run'):  # ugh!
+                        setattr(self, opt, strtobool(val))
+                    else:
+                        setattr(self, opt, val)
+                except ValueError as msg:
+                    raise DistutilsOptionError(msg)
+
+    # -- Command-line parsing methods ----------------------------------
+
+    def parse_command_line(self):
+        """Parse the setup script's command line, taken from the
+        'script_args' instance attribute (which defaults to 'sys.argv[1:]'
+        -- see 'setup()' in core.py).  This list is first processed for
+        "global options" -- options that set attributes of the Distribution
+        instance.  Then, it is alternately scanned for Distutils commands
+        and options for that command.  Each new command terminates the
+        options for the previous command.  The allowed options for a
+        command are determined by the 'user_options' attribute of the
+        command class -- thus, we have to be able to load command classes
+        in order to parse the command line.  Any error in that 'options'
+        attribute raises DistutilsGetoptError; any error on the
+        command-line raises DistutilsArgError.  If no Distutils commands
+        were found on the command line, raises DistutilsArgError.  Return
+        true if command-line was successfully parsed and we should carry
+        on with executing commands; false if no errors but we shouldn't
+        execute commands (currently, this only happens if user asks for
+        help).
+        """
+        #
+        # We now have enough information to show the Macintosh dialog
+        # that allows the user to interactively specify the "command line".
+        #
+        toplevel_options = self._get_toplevel_options()
+
+        # We have to parse the command line a bit at a time -- global
+        # options, then the first command, then its options, and so on --
+        # because each command will be handled by a different class, and
+        # the options that are valid for a particular class aren't known
+        # until we have loaded the command class, which doesn't happen
+        # until we know what the command is.
+
+        self.commands = []
+        parser = FancyGetopt(toplevel_options + self.display_options)
+        parser.set_negative_aliases(self.negative_opt)
+        parser.set_aliases({'licence': 'license'})
+        args = parser.getopt(args=self.script_args, object=self)
+        option_order = parser.get_option_order()
+        logging.getLogger().setLevel(logging.WARN - 10 * self.verbose)
+
+        # for display options we return immediately
+        if self.handle_display_options(option_order):
+            return
+        while args:
+            args = self._parse_command_opts(parser, args)
+            if args is None:  # user asked for help (and got it)
+                return
+
+        # Handle the cases of --help as a "global" option, ie.
+        # "setup.py --help" and "setup.py --help command ...".  For the
+        # former, we show global options (--verbose, --dry-run, etc.)
+        # and display-only options (--name, --version, etc.); for the
+        # latter, we omit the display-only options and show help for
+        # each command listed on the command line.
+        if self.help:
+            self._show_help(
+                parser, display_options=len(self.commands) == 0, commands=self.commands
+            )
+            return
+
+        # Oops, no commands found -- an end-user error
+        if not self.commands:
+            raise DistutilsArgError("no commands supplied")
+
+        # All is well: return true
+        return True
+
+    def _get_toplevel_options(self):
+        """Return the non-display options recognized at the top level.
+
+        This includes options that are recognized *only* at the top
+        level as well as options recognized for commands.
+        """
+        return self.global_options + [
+            (
+                "command-packages=",
+                None,
+                "list of packages that provide distutils commands",
+            ),
+        ]
+
+    def _parse_command_opts(self, parser, args):  # noqa: C901
+        """Parse the command-line options for a single command.
+        'parser' must be a FancyGetopt instance; 'args' must be the list
+        of arguments, starting with the current command (whose options
+        we are about to parse).  Returns a new version of 'args' with
+        the next command at the front of the list; will be the empty
+        list if there are no more commands on the command line.  Returns
+        None if the user asked for help on this command.
+        """
+        # late import because of mutual dependence between these modules
+        from distutils.cmd import Command
+
+        # Pull the current command from the head of the command line
+        command = args[0]
+        if not command_re.match(command):
+            raise SystemExit(f"invalid command name '{command}'")
+        self.commands.append(command)
+
+        # Dig up the command class that implements this command, so we
+        # 1) know that it's a valid command, and 2) know which options
+        # it takes.
+        try:
+            cmd_class = self.get_command_class(command)
+        except DistutilsModuleError as msg:
+            raise DistutilsArgError(msg)
+
+        # Require that the command class be derived from Command -- want
+        # to be sure that the basic "command" interface is implemented.
+        if not issubclass(cmd_class, Command):
+            raise DistutilsClassError(
+                f"command class {cmd_class} must subclass Command"
+            )
+
+        # Also make sure that the command object provides a list of its
+        # known options.
+        if not (
+            hasattr(cmd_class, 'user_options')
+            and isinstance(cmd_class.user_options, list)
+        ):
+            msg = (
+                "command class %s must provide "
+                "'user_options' attribute (a list of tuples)"
+            )
+            raise DistutilsClassError(msg % cmd_class)
+
+        # If the command class has a list of negative alias options,
+        # merge it in with the global negative aliases.
+        negative_opt = self.negative_opt
+        if hasattr(cmd_class, 'negative_opt'):
+            negative_opt = negative_opt.copy()
+            negative_opt.update(cmd_class.negative_opt)
+
+        # Check for help_options in command class.  They have a different
+        # format (tuple of four) so we need to preprocess them here.
+        if hasattr(cmd_class, 'help_options') and isinstance(
+            cmd_class.help_options, list
+        ):
+            help_options = fix_help_options(cmd_class.help_options)
+        else:
+            help_options = []
+
+        # All commands support the global options too, just by adding
+        # in 'global_options'.
+        parser.set_option_table(
+            self.global_options + cmd_class.user_options + help_options
+        )
+        parser.set_negative_aliases(negative_opt)
+        (args, opts) = parser.getopt(args[1:])
+        if hasattr(opts, 'help') and opts.help:
+            self._show_help(parser, display_options=False, commands=[cmd_class])
+            return
+
+        if hasattr(cmd_class, 'help_options') and isinstance(
+            cmd_class.help_options, list
+        ):
+            help_option_found = 0
+            for help_option, _short, _desc, func in cmd_class.help_options:
+                if hasattr(opts, parser.get_attr_name(help_option)):
+                    help_option_found = 1
+                    if callable(func):
+                        func()
+                    else:
+                        raise DistutilsClassError(
+                            f"invalid help function {func!r} for help option '{help_option}': "
+                            "must be a callable object (function, etc.)"
+                        )
+
+            if help_option_found:
+                return
+
+        # Put the options from the command-line into their official
+        # holding pen, the 'command_options' dictionary.
+        opt_dict = self.get_option_dict(command)
+        for name, value in vars(opts).items():
+            opt_dict[name] = ("command line", value)
+
+        return args
+
+    def finalize_options(self) -> None:
+        """Set final values for all the options on the Distribution
+        instance, analogous to the .finalize_options() method of Command
+        objects.
+        """
+        for attr in ('keywords', 'platforms'):
+            value = getattr(self.metadata, attr)
+            if value is None:
+                continue
+            if isinstance(value, str):
+                value = [elm.strip() for elm in value.split(',')]
+                setattr(self.metadata, attr, value)
+
+    def _show_help(
+        self, parser, global_options=True, display_options=True, commands: Iterable = ()
+    ):
+        """Show help for the setup script command-line in the form of
+        several lists of command-line options.  'parser' should be a
+        FancyGetopt instance; do not expect it to be returned in the
+        same state, as its option table will be reset to make it
+        generate the correct help text.
+
+        If 'global_options' is true, lists the global options:
+        --verbose, --dry-run, etc.  If 'display_options' is true, lists
+        the "display-only" options: --name, --version, etc.  Finally,
+        lists per-command help for every command name or command class
+        in 'commands'.
+        """
+        # late import because of mutual dependence between these modules
+        from distutils.cmd import Command
+        from distutils.core import gen_usage
+
+        if global_options:
+            if display_options:
+                options = self._get_toplevel_options()
+            else:
+                options = self.global_options
+            parser.set_option_table(options)
+            parser.print_help(self.common_usage + "\nGlobal options:")
+            print()
+
+        if display_options:
+            parser.set_option_table(self.display_options)
+            parser.print_help(
+                "Information display options (just display information, ignore any commands)"
+            )
+            print()
+
+        for command in commands:
+            if isinstance(command, type) and issubclass(command, Command):
+                klass = command
+            else:
+                klass = self.get_command_class(command)
+            if hasattr(klass, 'help_options') and isinstance(klass.help_options, list):
+                parser.set_option_table(
+                    klass.user_options + fix_help_options(klass.help_options)
+                )
+            else:
+                parser.set_option_table(klass.user_options)
+            parser.print_help(f"Options for '{klass.__name__}' command:")
+            print()
+
+        print(gen_usage(self.script_name))
+
+    def handle_display_options(self, option_order):
+        """If there were any non-global "display-only" options
+        (--help-commands or the metadata display options) on the command
+        line, display the requested info and return true; else return
+        false.
+        """
+        from distutils.core import gen_usage
+
+        # User just wants a list of commands -- we'll print it out and stop
+        # processing now (ie. if they ran "setup --help-commands foo bar",
+        # we ignore "foo bar").
+        if self.help_commands:
+            self.print_commands()
+            print()
+            print(gen_usage(self.script_name))
+            return 1
+
+        # If user supplied any of the "display metadata" options, then
+        # display that metadata in the order in which the user supplied the
+        # metadata options.
+        any_display_options = 0
+        is_display_option = set()
+        for option in self.display_options:
+            is_display_option.add(option[0])
+
+        for opt, val in option_order:
+            if val and opt in is_display_option:
+                opt = translate_longopt(opt)
+                value = getattr(self.metadata, "get_" + opt)()
+                if opt in ('keywords', 'platforms'):
+                    print(','.join(value))
+                elif opt in ('classifiers', 'provides', 'requires', 'obsoletes'):
+                    print('\n'.join(value))
+                else:
+                    print(value)
+                any_display_options = 1
+
+        return any_display_options
+
+    def print_command_list(self, commands, header, max_length) -> None:
+        """Print a subset of the list of all commands -- used by
+        'print_commands()'.
+        """
+        print(header + ":")
+
+        for cmd in commands:
+            klass = self.cmdclass.get(cmd)
+            if not klass:
+                klass = self.get_command_class(cmd)
+            try:
+                description = klass.description
+            except AttributeError:
+                description = "(no description available)"
+
+            print(f"  {cmd:<{max_length}}  {description}")
+
+    def print_commands(self) -> None:
+        """Print out a help message listing all available commands with a
+        description of each.  The list is divided into "standard commands"
+        (listed in distutils.command.__all__) and "extra commands"
+        (mentioned in self.cmdclass, but not a standard command).  The
+        descriptions come from the command class attribute
+        'description'.
+        """
+        import distutils.command
+
+        std_commands = distutils.command.__all__
+        is_std = set(std_commands)
+
+        extra_commands = [cmd for cmd in self.cmdclass.keys() if cmd not in is_std]
+
+        max_length = 0
+        for cmd in std_commands + extra_commands:
+            if len(cmd) > max_length:
+                max_length = len(cmd)
+
+        self.print_command_list(std_commands, "Standard commands", max_length)
+        if extra_commands:
+            print()
+            self.print_command_list(extra_commands, "Extra commands", max_length)
+
+    def get_command_list(self):
+        """Get a list of (command, description) tuples.
+        The list is divided into "standard commands" (listed in
+        distutils.command.__all__) and "extra commands" (mentioned in
+        self.cmdclass, but not a standard command).  The descriptions come
+        from the command class attribute 'description'.
+        """
+        # Currently this is only used on Mac OS, for the Mac-only GUI
+        # Distutils interface (by Jack Jansen)
+        import distutils.command
+
+        std_commands = distutils.command.__all__
+        is_std = set(std_commands)
+
+        extra_commands = [cmd for cmd in self.cmdclass.keys() if cmd not in is_std]
+
+        rv = []
+        for cmd in std_commands + extra_commands:
+            klass = self.cmdclass.get(cmd)
+            if not klass:
+                klass = self.get_command_class(cmd)
+            try:
+                description = klass.description
+            except AttributeError:
+                description = "(no description available)"
+            rv.append((cmd, description))
+        return rv
+
+    # -- Command class/object methods ----------------------------------
+
+    def get_command_packages(self):
+        """Return a list of packages from which commands are loaded."""
+        pkgs = self.command_packages
+        if not isinstance(pkgs, list):
+            if pkgs is None:
+                pkgs = ''
+            pkgs = [pkg.strip() for pkg in pkgs.split(',') if pkg != '']
+            if "distutils.command" not in pkgs:
+                pkgs.insert(0, "distutils.command")
+            self.command_packages = pkgs
+        return pkgs
+
+    def get_command_class(self, command: str) -> type[Command]:
+        """Return the class that implements the Distutils command named by
+        'command'.  First we check the 'cmdclass' dictionary; if the
+        command is mentioned there, we fetch the class object from the
+        dictionary and return it.  Otherwise we load the command module
+        ("distutils.command." + command) and fetch the command class from
+        the module.  The loaded class is also stored in 'cmdclass'
+        to speed future calls to 'get_command_class()'.
+
+        Raises DistutilsModuleError if the expected module could not be
+        found, or if that module does not define the expected class.
+        """
+        klass = self.cmdclass.get(command)
+        if klass:
+            return klass
+
+        for pkgname in self.get_command_packages():
+            module_name = f"{pkgname}.{command}"
+            klass_name = command
+
+            try:
+                __import__(module_name)
+                module = sys.modules[module_name]
+            except ImportError:
+                continue
+
+            try:
+                klass = getattr(module, klass_name)
+            except AttributeError:
+                raise DistutilsModuleError(
+                    f"invalid command '{command}' (no class '{klass_name}' in module '{module_name}')"
+                )
+
+            self.cmdclass[command] = klass
+            return klass
+
+        raise DistutilsModuleError(f"invalid command '{command}'")
+
+    @overload
+    def get_command_obj(
+        self, command: str, create: Literal[True] = True
+    ) -> Command: ...
+    @overload
+    def get_command_obj(
+        self, command: str, create: Literal[False]
+    ) -> Command | None: ...
+    def get_command_obj(self, command: str, create: bool = True) -> Command | None:
+        """Return the command object for 'command'.  Normally this object
+        is cached on a previous call to 'get_command_obj()'; if no command
+        object for 'command' is in the cache, then we either create and
+        return it (if 'create' is true) or return None.
+        """
+        cmd_obj = self.command_obj.get(command)
+        if not cmd_obj and create:
+            if DEBUG:
+                self.announce(
+                    "Distribution.get_command_obj(): "
+                    f"creating '{command}' command object"
+                )
+
+            klass = self.get_command_class(command)
+            cmd_obj = self.command_obj[command] = klass(self)
+            self.have_run[command] = False
+
+            # Set any options that were supplied in config files
+            # or on the command line.  (NB. support for error
+            # reporting is lame here: any errors aren't reported
+            # until 'finalize_options()' is called, which means
+            # we won't report the source of the error.)
+            options = self.command_options.get(command)
+            if options:
+                self._set_command_options(cmd_obj, options)
+
+        return cmd_obj
+
+    def _set_command_options(self, command_obj, option_dict=None):  # noqa: C901
+        """Set the options for 'command_obj' from 'option_dict'.  Basically
+        this means copying elements of a dictionary ('option_dict') to
+        attributes of an instance ('command').
+
+        'command_obj' must be a Command instance.  If 'option_dict' is not
+        supplied, uses the standard option dictionary for this command
+        (from 'self.command_options').
+        """
+        command_name = command_obj.get_command_name()
+        if option_dict is None:
+            option_dict = self.get_option_dict(command_name)
+
+        if DEBUG:
+            self.announce(f"  setting options for '{command_name}' command:")
+        for option, (source, value) in option_dict.items():
+            if DEBUG:
+                self.announce(f"    {option} = {value} (from {source})")
+            try:
+                bool_opts = [translate_longopt(o) for o in command_obj.boolean_options]
+            except AttributeError:
+                bool_opts = []
+            try:
+                neg_opt = command_obj.negative_opt
+            except AttributeError:
+                neg_opt = {}
+
+            try:
+                is_string = isinstance(value, str)
+                if option in neg_opt and is_string:
+                    setattr(command_obj, neg_opt[option], not strtobool(value))
+                elif option in bool_opts and is_string:
+                    setattr(command_obj, option, strtobool(value))
+                elif hasattr(command_obj, option):
+                    setattr(command_obj, option, value)
+                else:
+                    raise DistutilsOptionError(
+                        f"error in {source}: command '{command_name}' has no such option '{option}'"
+                    )
+            except ValueError as msg:
+                raise DistutilsOptionError(msg)
+
+    @overload
+    def reinitialize_command(
+        self, command: str, reinit_subcommands: bool = False
+    ) -> Command: ...
+    @overload
+    def reinitialize_command(
+        self, command: _CommandT, reinit_subcommands: bool = False
+    ) -> _CommandT: ...
+    def reinitialize_command(
+        self, command: str | Command, reinit_subcommands=False
+    ) -> Command:
+        """Reinitializes a command to the state it was in when first
+        returned by 'get_command_obj()': ie., initialized but not yet
+        finalized.  This provides the opportunity to sneak option
+        values in programmatically, overriding or supplementing
+        user-supplied values from the config files and command line.
+        You'll have to re-finalize the command object (by calling
+        'finalize_options()' or 'ensure_finalized()') before using it for
+        real.
+
+        'command' should be a command name (string) or command object.  If
+        'reinit_subcommands' is true, also reinitializes the command's
+        sub-commands, as declared by the 'sub_commands' class attribute (if
+        it has one).  See the "install" command for an example.  Only
+        reinitializes the sub-commands that actually matter, ie. those
+        whose test predicates return true.
+
+        Returns the reinitialized command object.
+        """
+        from distutils.cmd import Command
+
+        if not isinstance(command, Command):
+            command_name = command
+            command = self.get_command_obj(command_name)
+        else:
+            command_name = command.get_command_name()
+
+        if not command.finalized:
+            return command
+        command.initialize_options()
+        command.finalized = False
+        self.have_run[command_name] = False
+        self._set_command_options(command)
+
+        if reinit_subcommands:
+            for sub in command.get_sub_commands():
+                self.reinitialize_command(sub, reinit_subcommands)
+
+        return command
+
+    # -- Methods that operate on the Distribution ----------------------
+
+    def announce(self, msg, level: int = logging.INFO) -> None:
+        log.log(level, msg)
+
+    def run_commands(self) -> None:
+        """Run each command that was seen on the setup script command line.
+        Uses the list of commands found and cache of command objects
+        created by 'get_command_obj()'.
+        """
+        for cmd in self.commands:
+            self.run_command(cmd)
+
+    # -- Methods that operate on its Commands --------------------------
+
+    def run_command(self, command: str) -> None:
+        """Do whatever it takes to run a command (including nothing at all,
+        if the command has already been run).  Specifically: if we have
+        already created and run the command named by 'command', return
+        silently without doing anything.  If the command named by 'command'
+        doesn't even have a command object yet, create one.  Then invoke
+        'run()' on that command object (or an existing one).
+        """
+        # Already been here, done that? then return silently.
+        if self.have_run.get(command):
+            return
+
+        log.info("running %s", command)
+        cmd_obj = self.get_command_obj(command)
+        cmd_obj.ensure_finalized()
+        cmd_obj.run()
+        self.have_run[command] = True
+
+    # -- Distribution query methods ------------------------------------
+
+    def has_pure_modules(self) -> bool:
+        return len(self.packages or self.py_modules or []) > 0
+
+    def has_ext_modules(self) -> bool:
+        return self.ext_modules and len(self.ext_modules) > 0
+
+    def has_c_libraries(self) -> bool:
+        return self.libraries and len(self.libraries) > 0
+
+    def has_modules(self) -> bool:
+        return self.has_pure_modules() or self.has_ext_modules()
+
+    def has_headers(self) -> bool:
+        return self.headers and len(self.headers) > 0
+
+    def has_scripts(self) -> bool:
+        return self.scripts and len(self.scripts) > 0
+
+    def has_data_files(self) -> bool:
+        return self.data_files and len(self.data_files) > 0
+
+    def is_pure(self) -> bool:
+        return (
+            self.has_pure_modules()
+            and not self.has_ext_modules()
+            and not self.has_c_libraries()
+        )
+
+    # -- Metadata query methods ----------------------------------------
+
+    # If you're looking for 'get_name()', 'get_version()', and so forth,
+    # they are defined in a sneaky way: the constructor binds self.get_XXX
+    # to self.metadata.get_XXX.  The actual code is in the
+    # DistributionMetadata class, below.
+    if TYPE_CHECKING:
+        # Unfortunately this means we need to specify them manually or not expose statically
+        def _(self) -> None:
+            self.get_name = self.metadata.get_name
+            self.get_version = self.metadata.get_version
+            self.get_fullname = self.metadata.get_fullname
+            self.get_author = self.metadata.get_author
+            self.get_author_email = self.metadata.get_author_email
+            self.get_maintainer = self.metadata.get_maintainer
+            self.get_maintainer_email = self.metadata.get_maintainer_email
+            self.get_contact = self.metadata.get_contact
+            self.get_contact_email = self.metadata.get_contact_email
+            self.get_url = self.metadata.get_url
+            self.get_license = self.metadata.get_license
+            self.get_licence = self.metadata.get_licence
+            self.get_description = self.metadata.get_description
+            self.get_long_description = self.metadata.get_long_description
+            self.get_keywords = self.metadata.get_keywords
+            self.get_platforms = self.metadata.get_platforms
+            self.get_classifiers = self.metadata.get_classifiers
+            self.get_download_url = self.metadata.get_download_url
+            self.get_requires = self.metadata.get_requires
+            self.get_provides = self.metadata.get_provides
+            self.get_obsoletes = self.metadata.get_obsoletes
+
+        # Default attributes generated in __init__ from self.display_option_names
+        help_commands: bool
+        name: str | Literal[False]
+        version: str | Literal[False]
+        fullname: str | Literal[False]
+        author: str | Literal[False]
+        author_email: str | Literal[False]
+        maintainer: str | Literal[False]
+        maintainer_email: str | Literal[False]
+        contact: str | Literal[False]
+        contact_email: str | Literal[False]
+        url: str | Literal[False]
+        license: str | Literal[False]
+        licence: str | Literal[False]
+        description: str | Literal[False]
+        long_description: str | Literal[False]
+        platforms: str | list[str] | Literal[False]
+        classifiers: str | list[str] | Literal[False]
+        keywords: str | list[str] | Literal[False]
+        provides: list[str] | Literal[False]
+        requires: list[str] | Literal[False]
+        obsoletes: list[str] | Literal[False]
+
+
+class DistributionMetadata:
+    """Dummy class to hold the distribution meta-data: name, version,
+    author, and so forth.
+    """
+
+    _METHOD_BASENAMES = (
+        "name",
+        "version",
+        "author",
+        "author_email",
+        "maintainer",
+        "maintainer_email",
+        "url",
+        "license",
+        "description",
+        "long_description",
+        "keywords",
+        "platforms",
+        "fullname",
+        "contact",
+        "contact_email",
+        "classifiers",
+        "download_url",
+        # PEP 314
+        "provides",
+        "requires",
+        "obsoletes",
+    )
+
+    def __init__(
+        self, path: str | bytes | os.PathLike[str] | os.PathLike[bytes] | None = None
+    ) -> None:
+        if path is not None:
+            self.read_pkg_file(open(path))
+        else:
+            self.name: str | None = None
+            self.version: str | None = None
+            self.author: str | None = None
+            self.author_email: str | None = None
+            self.maintainer: str | None = None
+            self.maintainer_email: str | None = None
+            self.url: str | None = None
+            self.license: str | None = None
+            self.description: str | None = None
+            self.long_description: str | None = None
+            self.keywords: str | list[str] | None = None
+            self.platforms: str | list[str] | None = None
+            self.classifiers: str | list[str] | None = None
+            self.download_url: str | None = None
+            # PEP 314
+            self.provides: str | list[str] | None = None
+            self.requires: str | list[str] | None = None
+            self.obsoletes: str | list[str] | None = None
+
+    def read_pkg_file(self, file: IO[str]) -> None:
+        """Reads the metadata values from a file object."""
+        msg = message_from_file(file)
+
+        def _read_field(name: str) -> str | None:
+            value = msg[name]
+            if value and value != "UNKNOWN":
+                return value
+            return None
+
+        def _read_list(name):
+            values = msg.get_all(name, None)
+            if values == []:
+                return None
+            return values
+
+        metadata_version = msg['metadata-version']
+        self.name = _read_field('name')
+        self.version = _read_field('version')
+        self.description = _read_field('summary')
+        # we are filling author only.
+        self.author = _read_field('author')
+        self.maintainer = None
+        self.author_email = _read_field('author-email')
+        self.maintainer_email = None
+        self.url = _read_field('home-page')
+        self.license = _read_field('license')
+
+        if 'download-url' in msg:
+            self.download_url = _read_field('download-url')
+        else:
+            self.download_url = None
+
+        self.long_description = _read_field('description')
+        self.description = _read_field('summary')
+
+        if 'keywords' in msg:
+            self.keywords = _read_field('keywords').split(',')
+
+        self.platforms = _read_list('platform')
+        self.classifiers = _read_list('classifier')
+
+        # PEP 314 - these fields only exist in 1.1
+        if metadata_version == '1.1':
+            self.requires = _read_list('requires')
+            self.provides = _read_list('provides')
+            self.obsoletes = _read_list('obsoletes')
+        else:
+            self.requires = None
+            self.provides = None
+            self.obsoletes = None
+
+    def write_pkg_info(self, base_dir: str | os.PathLike[str]) -> None:
+        """Write the PKG-INFO file into the release tree."""
+        with open(
+            os.path.join(base_dir, 'PKG-INFO'), 'w', encoding='UTF-8'
+        ) as pkg_info:
+            self.write_pkg_file(pkg_info)
+
+    def write_pkg_file(self, file: SupportsWrite[str]) -> None:
+        """Write the PKG-INFO format data to a file object."""
+        version = '1.0'
+        if (
+            self.provides
+            or self.requires
+            or self.obsoletes
+            or self.classifiers
+            or self.download_url
+        ):
+            version = '1.1'
+
+        # required fields
+        file.write(f'Metadata-Version: {version}\n')
+        file.write(f'Name: {self.get_name()}\n')
+        file.write(f'Version: {self.get_version()}\n')
+
+        def maybe_write(header, val):
+            if val:
+                file.write(f"{header}: {val}\n")
+
+        # optional fields
+        maybe_write("Summary", self.get_description())
+        maybe_write("Home-page", self.get_url())
+        maybe_write("Author", self.get_contact())
+        maybe_write("Author-email", self.get_contact_email())
+        maybe_write("License", self.get_license())
+        maybe_write("Download-URL", self.download_url)
+        maybe_write("Description", rfc822_escape(self.get_long_description() or ""))
+        maybe_write("Keywords", ",".join(self.get_keywords()))
+
+        self._write_list(file, 'Platform', self.get_platforms())
+        self._write_list(file, 'Classifier', self.get_classifiers())
+
+        # PEP 314
+        self._write_list(file, 'Requires', self.get_requires())
+        self._write_list(file, 'Provides', self.get_provides())
+        self._write_list(file, 'Obsoletes', self.get_obsoletes())
+
+    def _write_list(self, file, name, values):
+        values = values or []
+        for value in values:
+            file.write(f'{name}: {value}\n')
+
+    # -- Metadata query methods ----------------------------------------
+
+    def get_name(self) -> str:
+        return self.name or "UNKNOWN"
+
+    def get_version(self) -> str:
+        return self.version or "0.0.0"
+
+    def get_fullname(self) -> str:
+        return self._fullname(self.get_name(), self.get_version())
+
+    @staticmethod
+    def _fullname(name: str, version: str) -> str:
+        """
+        >>> DistributionMetadata._fullname('setup.tools', '1.0-2')
+        'setup_tools-1.0.post2'
+        >>> DistributionMetadata._fullname('setup-tools', '1.2post2')
+        'setup_tools-1.2.post2'
+        >>> DistributionMetadata._fullname('setup-tools', '1.0-r2')
+        'setup_tools-1.0.post2'
+        >>> DistributionMetadata._fullname('setup.tools', '1.0.post')
+        'setup_tools-1.0.post0'
+        >>> DistributionMetadata._fullname('setup.tools', '1.0+ubuntu-1')
+        'setup_tools-1.0+ubuntu.1'
+        """
+        return "{}-{}".format(
+            canonicalize_name(name).replace('-', '_'),
+            canonicalize_version(version, strip_trailing_zero=False),
+        )
+
+    def get_author(self) -> str | None:
+        return self.author
+
+    def get_author_email(self) -> str | None:
+        return self.author_email
+
+    def get_maintainer(self) -> str | None:
+        return self.maintainer
+
+    def get_maintainer_email(self) -> str | None:
+        return self.maintainer_email
+
+    def get_contact(self) -> str | None:
+        return self.maintainer or self.author
+
+    def get_contact_email(self) -> str | None:
+        return self.maintainer_email or self.author_email
+
+    def get_url(self) -> str | None:
+        return self.url
+
+    def get_license(self) -> str | None:
+        return self.license
+
+    get_licence = get_license
+
+    def get_description(self) -> str | None:
+        return self.description
+
+    def get_long_description(self) -> str | None:
+        return self.long_description
+
+    def get_keywords(self) -> str | list[str]:
+        return self.keywords or []
+
+    def set_keywords(self, value: str | Iterable[str]) -> None:
+        self.keywords = _ensure_list(value, 'keywords')
+
+    def get_platforms(self) -> str | list[str] | None:
+        return self.platforms
+
+    def set_platforms(self, value: str | Iterable[str]) -> None:
+        self.platforms = _ensure_list(value, 'platforms')
+
+    def get_classifiers(self) -> str | list[str]:
+        return self.classifiers or []
+
+    def set_classifiers(self, value: str | Iterable[str]) -> None:
+        self.classifiers = _ensure_list(value, 'classifiers')
+
+    def get_download_url(self) -> str | None:
+        return self.download_url
+
+    # PEP 314
+    def get_requires(self) -> str | list[str]:
+        return self.requires or []
+
+    def set_requires(self, value: Iterable[str]) -> None:
+        import distutils.versionpredicate
+
+        for v in value:
+            distutils.versionpredicate.VersionPredicate(v)
+        self.requires = list(value)
+
+    def get_provides(self) -> str | list[str]:
+        return self.provides or []
+
+    def set_provides(self, value: Iterable[str]) -> None:
+        value = [v.strip() for v in value]
+        for v in value:
+            import distutils.versionpredicate
+
+            distutils.versionpredicate.split_provision(v)
+        self.provides = value
+
+    def get_obsoletes(self) -> str | list[str]:
+        return self.obsoletes or []
+
+    def set_obsoletes(self, value: Iterable[str]) -> None:
+        import distutils.versionpredicate
+
+        for v in value:
+            distutils.versionpredicate.VersionPredicate(v)
+        self.obsoletes = list(value)
+
+
+def fix_help_options(options):
+    """Convert a 4-tuple 'help_options' list as found in various command
+    classes to the 3-tuple form required by FancyGetopt.
+    """
+    return [opt[0:3] for opt in options]
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/errors.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/errors.py
new file mode 100644
index 00000000..409d21fa
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/errors.py
@@ -0,0 +1,108 @@
+"""
+Exceptions used by the Distutils modules.
+
+Distutils modules may raise these or standard exceptions,
+including :exc:`SystemExit`.
+"""
+
+# compiler exceptions aliased for compatibility
+from .compilers.C.errors import CompileError as CompileError
+from .compilers.C.errors import Error as _Error
+from .compilers.C.errors import LibError as LibError
+from .compilers.C.errors import LinkError as LinkError
+from .compilers.C.errors import PreprocessError as PreprocessError
+from .compilers.C.errors import UnknownFileType as _UnknownFileType
+
+CCompilerError = _Error
+UnknownFileError = _UnknownFileType
+
+
+class DistutilsError(Exception):
+    """The root of all Distutils evil."""
+
+    pass
+
+
+class DistutilsModuleError(DistutilsError):
+    """Unable to load an expected module, or to find an expected class
+    within some module (in particular, command modules and classes)."""
+
+    pass
+
+
+class DistutilsClassError(DistutilsError):
+    """Some command class (or possibly distribution class, if anyone
+    feels a need to subclass Distribution) is found not to be holding
+    up its end of the bargain, ie. implementing some part of the
+    "command "interface."""
+
+    pass
+
+
+class DistutilsGetoptError(DistutilsError):
+    """The option table provided to 'fancy_getopt()' is bogus."""
+
+    pass
+
+
+class DistutilsArgError(DistutilsError):
+    """Raised by fancy_getopt in response to getopt.error -- ie. an
+    error in the command line usage."""
+
+    pass
+
+
+class DistutilsFileError(DistutilsError):
+    """Any problems in the filesystem: expected file not found, etc.
+    Typically this is for problems that we detect before OSError
+    could be raised."""
+
+    pass
+
+
+class DistutilsOptionError(DistutilsError):
+    """Syntactic/semantic errors in command options, such as use of
+    mutually conflicting options, or inconsistent options,
+    badly-spelled values, etc.  No distinction is made between option
+    values originating in the setup script, the command line, config
+    files, or what-have-you -- but if we *know* something originated in
+    the setup script, we'll raise DistutilsSetupError instead."""
+
+    pass
+
+
+class DistutilsSetupError(DistutilsError):
+    """For errors that can be definitely blamed on the setup script,
+    such as invalid keyword arguments to 'setup()'."""
+
+    pass
+
+
+class DistutilsPlatformError(DistutilsError):
+    """We don't know how to do something on the current platform (but
+    we do know how to do it on some platform) -- eg. trying to compile
+    C files on a platform not supported by a CCompiler subclass."""
+
+    pass
+
+
+class DistutilsExecError(DistutilsError):
+    """Any problems executing an external program (such as the C
+    compiler, when compiling C files)."""
+
+    pass
+
+
+class DistutilsInternalError(DistutilsError):
+    """Internal inconsistencies or impossibilities (obviously, this
+    should never be seen if the code is working!)."""
+
+    pass
+
+
+class DistutilsTemplateError(DistutilsError):
+    """Syntax error in a file list template."""
+
+
+class DistutilsByteCompileError(DistutilsError):
+    """Byte compile error."""
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/extension.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/extension.py
new file mode 100644
index 00000000..f5141126
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/extension.py
@@ -0,0 +1,258 @@
+"""distutils.extension
+
+Provides the Extension class, used to describe C/C++ extension
+modules in setup scripts."""
+
+from __future__ import annotations
+
+import os
+import warnings
+from collections.abc import Iterable
+
+# This class is really only used by the "build_ext" command, so it might
+# make sense to put it in distutils.command.build_ext.  However, that
+# module is already big enough, and I want to make this class a bit more
+# complex to simplify some common cases ("foo" module in "foo.c") and do
+# better error-checking ("foo.c" actually exists).
+#
+# Also, putting this in build_ext.py means every setup script would have to
+# import that large-ish module (indirectly, through distutils.core) in
+# order to do anything.
+
+
+class Extension:
+    """Just a collection of attributes that describes an extension
+    module and everything needed to build it (hopefully in a portable
+    way, but there are hooks that let you be as unportable as you need).
+
+    Instance attributes:
+      name : string
+        the full name of the extension, including any packages -- ie.
+        *not* a filename or pathname, but Python dotted name
+      sources : Iterable[string | os.PathLike]
+        iterable of source filenames (except strings, which could be misinterpreted
+        as a single filename), relative to the distribution root (where the setup
+        script lives), in Unix form (slash-separated) for portability. Can be any
+        non-string iterable (list, tuple, set, etc.) containing strings or
+        PathLike objects. Source files may be C, C++, SWIG (.i), platform-specific
+        resource files, or whatever else is recognized by the "build_ext" command
+        as source for a Python extension.
+      include_dirs : [string]
+        list of directories to search for C/C++ header files (in Unix
+        form for portability)
+      define_macros : [(name : string, value : string|None)]
+        list of macros to define; each macro is defined using a 2-tuple,
+        where 'value' is either the string to define it to or None to
+        define it without a particular value (equivalent of "#define
+        FOO" in source or -DFOO on Unix C compiler command line)
+      undef_macros : [string]
+        list of macros to undefine explicitly
+      library_dirs : [string]
+        list of directories to search for C/C++ libraries at link time
+      libraries : [string]
+        list of library names (not filenames or paths) to link against
+      runtime_library_dirs : [string]
+        list of directories to search for C/C++ libraries at run time
+        (for shared extensions, this is when the extension is loaded)
+      extra_objects : [string]
+        list of extra files to link with (eg. object files not implied
+        by 'sources', static library that must be explicitly specified,
+        binary resource files, etc.)
+      extra_compile_args : [string]
+        any extra platform- and compiler-specific information to use
+        when compiling the source files in 'sources'.  For platforms and
+        compilers where "command line" makes sense, this is typically a
+        list of command-line arguments, but for other platforms it could
+        be anything.
+      extra_link_args : [string]
+        any extra platform- and compiler-specific information to use
+        when linking object files together to create the extension (or
+        to create a new static Python interpreter).  Similar
+        interpretation as for 'extra_compile_args'.
+      export_symbols : [string]
+        list of symbols to be exported from a shared extension.  Not
+        used on all platforms, and not generally necessary for Python
+        extensions, which typically export exactly one symbol: "init" +
+        extension_name.
+      swig_opts : [string]
+        any extra options to pass to SWIG if a source file has the .i
+        extension.
+      depends : [string]
+        list of files that the extension depends on
+      language : string
+        extension language (i.e. "c", "c++", "objc"). Will be detected
+        from the source extensions if not provided.
+      optional : boolean
+        specifies that a build failure in the extension should not abort the
+        build process, but simply not install the failing extension.
+    """
+
+    # When adding arguments to this constructor, be sure to update
+    # setup_keywords in core.py.
+    def __init__(
+        self,
+        name: str,
+        sources: Iterable[str | os.PathLike[str]],
+        include_dirs: list[str] | None = None,
+        define_macros: list[tuple[str, str | None]] | None = None,
+        undef_macros: list[str] | None = None,
+        library_dirs: list[str] | None = None,
+        libraries: list[str] | None = None,
+        runtime_library_dirs: list[str] | None = None,
+        extra_objects: list[str] | None = None,
+        extra_compile_args: list[str] | None = None,
+        extra_link_args: list[str] | None = None,
+        export_symbols: list[str] | None = None,
+        swig_opts: list[str] | None = None,
+        depends: list[str] | None = None,
+        language: str | None = None,
+        optional: bool | None = None,
+        **kw,  # To catch unknown keywords
+    ):
+        if not isinstance(name, str):
+            raise TypeError("'name' must be a string")
+
+        # handle the string case first; since strings are iterable, disallow them
+        if isinstance(sources, str):
+            raise TypeError(
+                "'sources' must be an iterable of strings or PathLike objects, not a string"
+            )
+
+        # now we check if it's iterable and contains valid types
+        try:
+            self.sources = list(map(os.fspath, sources))
+        except TypeError:
+            raise TypeError(
+                "'sources' must be an iterable of strings or PathLike objects"
+            )
+
+        self.name = name
+        self.include_dirs = include_dirs or []
+        self.define_macros = define_macros or []
+        self.undef_macros = undef_macros or []
+        self.library_dirs = library_dirs or []
+        self.libraries = libraries or []
+        self.runtime_library_dirs = runtime_library_dirs or []
+        self.extra_objects = extra_objects or []
+        self.extra_compile_args = extra_compile_args or []
+        self.extra_link_args = extra_link_args or []
+        self.export_symbols = export_symbols or []
+        self.swig_opts = swig_opts or []
+        self.depends = depends or []
+        self.language = language
+        self.optional = optional
+
+        # If there are unknown keyword options, warn about them
+        if len(kw) > 0:
+            options = [repr(option) for option in kw]
+            options = ', '.join(sorted(options))
+            msg = f"Unknown Extension options: {options}"
+            warnings.warn(msg)
+
+    def __repr__(self):
+        return f'<{self.__class__.__module__}.{self.__class__.__qualname__}({self.name!r}) at {id(self):#x}>'
+
+
+def read_setup_file(filename):  # noqa: C901
+    """Reads a Setup file and returns Extension instances."""
+    from distutils.sysconfig import _variable_rx, expand_makefile_vars, parse_makefile
+    from distutils.text_file import TextFile
+    from distutils.util import split_quoted
+
+    # First pass over the file to gather "VAR = VALUE" assignments.
+    vars = parse_makefile(filename)
+
+    # Second pass to gobble up the real content: lines of the form
+    #   <module> ... [<sourcefile> ...] [<cpparg> ...] [<library> ...]
+    file = TextFile(
+        filename,
+        strip_comments=True,
+        skip_blanks=True,
+        join_lines=True,
+        lstrip_ws=True,
+        rstrip_ws=True,
+    )
+    try:
+        extensions = []
+
+        while True:
+            line = file.readline()
+            if line is None:  # eof
+                break
+            if _variable_rx.match(line):  # VAR=VALUE, handled in first pass
+                continue
+
+            if line[0] == line[-1] == "*":
+                file.warn(f"'{line}' lines not handled yet")
+                continue
+
+            line = expand_makefile_vars(line, vars)
+            words = split_quoted(line)
+
+            # NB. this parses a slightly different syntax than the old
+            # makesetup script: here, there must be exactly one extension per
+            # line, and it must be the first word of the line.  I have no idea
+            # why the old syntax supported multiple extensions per line, as
+            # they all wind up being the same.
+
+            module = words[0]
+            ext = Extension(module, [])
+            append_next_word = None
+
+            for word in words[1:]:
+                if append_next_word is not None:
+                    append_next_word.append(word)
+                    append_next_word = None
+                    continue
+
+                suffix = os.path.splitext(word)[1]
+                switch = word[0:2]
+                value = word[2:]
+
+                if suffix in (".c", ".cc", ".cpp", ".cxx", ".c++", ".m", ".mm"):
+                    # hmm, should we do something about C vs. C++ sources?
+                    # or leave it up to the CCompiler implementation to
+                    # worry about?
+                    ext.sources.append(word)
+                elif switch == "-I":
+                    ext.include_dirs.append(value)
+                elif switch == "-D":
+                    equals = value.find("=")
+                    if equals == -1:  # bare "-DFOO" -- no value
+                        ext.define_macros.append((value, None))
+                    else:  # "-DFOO=blah"
+                        ext.define_macros.append((value[0:equals], value[equals + 2 :]))
+                elif switch == "-U":
+                    ext.undef_macros.append(value)
+                elif switch == "-C":  # only here 'cause makesetup has it!
+                    ext.extra_compile_args.append(word)
+                elif switch == "-l":
+                    ext.libraries.append(value)
+                elif switch == "-L":
+                    ext.library_dirs.append(value)
+                elif switch == "-R":
+                    ext.runtime_library_dirs.append(value)
+                elif word == "-rpath":
+                    append_next_word = ext.runtime_library_dirs
+                elif word == "-Xlinker":
+                    append_next_word = ext.extra_link_args
+                elif word == "-Xcompiler":
+                    append_next_word = ext.extra_compile_args
+                elif switch == "-u":
+                    ext.extra_link_args.append(word)
+                    if not value:
+                        append_next_word = ext.extra_link_args
+                elif suffix in (".a", ".so", ".sl", ".o", ".dylib"):
+                    # NB. a really faithful emulation of makesetup would
+                    # append a .o file to extra_objects only if it
+                    # had a slash in it; otherwise, it would s/.o/.c/
+                    # and append it to sources.  Hmmmm.
+                    ext.extra_objects.append(word)
+                else:
+                    file.warn(f"unrecognized argument '{word}'")
+
+            extensions.append(ext)
+    finally:
+        file.close()
+
+    return extensions
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/fancy_getopt.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/fancy_getopt.py
new file mode 100644
index 00000000..1a1d3a05
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/fancy_getopt.py
@@ -0,0 +1,471 @@
+"""distutils.fancy_getopt
+
+Wrapper around the standard getopt module that provides the following
+additional features:
+  * short and long options are tied together
+  * options have help strings, so fancy_getopt could potentially
+    create a complete usage summary
+  * options set attributes of a passed-in object
+"""
+
+from __future__ import annotations
+
+import getopt
+import re
+import string
+import sys
+from collections.abc import Sequence
+from typing import Any
+
+from .errors import DistutilsArgError, DistutilsGetoptError
+
+# Much like command_re in distutils.core, this is close to but not quite
+# the same as a Python NAME -- except, in the spirit of most GNU
+# utilities, we use '-' in place of '_'.  (The spirit of LISP lives on!)
+# The similarities to NAME are again not a coincidence...
+longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)'
+longopt_re = re.compile(rf'^{longopt_pat}$')
+
+# For recognizing "negative alias" options, eg. "quiet=!verbose"
+neg_alias_re = re.compile(f"^({longopt_pat})=!({longopt_pat})$")
+
+# This is used to translate long options to legitimate Python identifiers
+# (for use as attributes of some object).
+longopt_xlate = str.maketrans('-', '_')
+
+
+class FancyGetopt:
+    """Wrapper around the standard 'getopt()' module that provides some
+    handy extra functionality:
+      * short and long options are tied together
+      * options have help strings, and help text can be assembled
+        from them
+      * options set attributes of a passed-in object
+      * boolean options can have "negative aliases" -- eg. if
+        --quiet is the "negative alias" of --verbose, then "--quiet"
+        on the command line sets 'verbose' to false
+    """
+
+    def __init__(self, option_table=None):
+        # The option table is (currently) a list of tuples.  The
+        # tuples may have 3 or four values:
+        #   (long_option, short_option, help_string [, repeatable])
+        # if an option takes an argument, its long_option should have '='
+        # appended; short_option should just be a single character, no ':'
+        # in any case.  If a long_option doesn't have a corresponding
+        # short_option, short_option should be None.  All option tuples
+        # must have long options.
+        self.option_table = option_table
+
+        # 'option_index' maps long option names to entries in the option
+        # table (ie. those 3-tuples).
+        self.option_index = {}
+        if self.option_table:
+            self._build_index()
+
+        # 'alias' records (duh) alias options; {'foo': 'bar'} means
+        # --foo is an alias for --bar
+        self.alias = {}
+
+        # 'negative_alias' keeps track of options that are the boolean
+        # opposite of some other option
+        self.negative_alias = {}
+
+        # These keep track of the information in the option table.  We
+        # don't actually populate these structures until we're ready to
+        # parse the command-line, since the 'option_table' passed in here
+        # isn't necessarily the final word.
+        self.short_opts = []
+        self.long_opts = []
+        self.short2long = {}
+        self.attr_name = {}
+        self.takes_arg = {}
+
+        # And 'option_order' is filled up in 'getopt()'; it records the
+        # original order of options (and their values) on the command-line,
+        # but expands short options, converts aliases, etc.
+        self.option_order = []
+
+    def _build_index(self):
+        self.option_index.clear()
+        for option in self.option_table:
+            self.option_index[option[0]] = option
+
+    def set_option_table(self, option_table):
+        self.option_table = option_table
+        self._build_index()
+
+    def add_option(self, long_option, short_option=None, help_string=None):
+        if long_option in self.option_index:
+            raise DistutilsGetoptError(
+                f"option conflict: already an option '{long_option}'"
+            )
+        else:
+            option = (long_option, short_option, help_string)
+            self.option_table.append(option)
+            self.option_index[long_option] = option
+
+    def has_option(self, long_option):
+        """Return true if the option table for this parser has an
+        option with long name 'long_option'."""
+        return long_option in self.option_index
+
+    def get_attr_name(self, long_option):
+        """Translate long option name 'long_option' to the form it
+        has as an attribute of some object: ie., translate hyphens
+        to underscores."""
+        return long_option.translate(longopt_xlate)
+
+    def _check_alias_dict(self, aliases, what):
+        assert isinstance(aliases, dict)
+        for alias, opt in aliases.items():
+            if alias not in self.option_index:
+                raise DistutilsGetoptError(
+                    f"invalid {what} '{alias}': option '{alias}' not defined"
+                )
+            if opt not in self.option_index:
+                raise DistutilsGetoptError(
+                    f"invalid {what} '{alias}': aliased option '{opt}' not defined"
+                )
+
+    def set_aliases(self, alias):
+        """Set the aliases for this option parser."""
+        self._check_alias_dict(alias, "alias")
+        self.alias = alias
+
+    def set_negative_aliases(self, negative_alias):
+        """Set the negative aliases for this option parser.
+        'negative_alias' should be a dictionary mapping option names to
+        option names, both the key and value must already be defined
+        in the option table."""
+        self._check_alias_dict(negative_alias, "negative alias")
+        self.negative_alias = negative_alias
+
+    def _grok_option_table(self):  # noqa: C901
+        """Populate the various data structures that keep tabs on the
+        option table.  Called by 'getopt()' before it can do anything
+        worthwhile.
+        """
+        self.long_opts = []
+        self.short_opts = []
+        self.short2long.clear()
+        self.repeat = {}
+
+        for option in self.option_table:
+            if len(option) == 3:
+                long, short, help = option
+                repeat = 0
+            elif len(option) == 4:
+                long, short, help, repeat = option
+            else:
+                # the option table is part of the code, so simply
+                # assert that it is correct
+                raise ValueError(f"invalid option tuple: {option!r}")
+
+            # Type- and value-check the option names
+            if not isinstance(long, str) or len(long) < 2:
+                raise DistutilsGetoptError(
+                    f"invalid long option '{long}': must be a string of length >= 2"
+                )
+
+            if not ((short is None) or (isinstance(short, str) and len(short) == 1)):
+                raise DistutilsGetoptError(
+                    f"invalid short option '{short}': must a single character or None"
+                )
+
+            self.repeat[long] = repeat
+            self.long_opts.append(long)
+
+            if long[-1] == '=':  # option takes an argument?
+                if short:
+                    short = short + ':'
+                long = long[0:-1]
+                self.takes_arg[long] = True
+            else:
+                # Is option is a "negative alias" for some other option (eg.
+                # "quiet" == "!verbose")?
+                alias_to = self.negative_alias.get(long)
+                if alias_to is not None:
+                    if self.takes_arg[alias_to]:
+                        raise DistutilsGetoptError(
+                            f"invalid negative alias '{long}': "
+                            f"aliased option '{alias_to}' takes a value"
+                        )
+
+                    self.long_opts[-1] = long  # XXX redundant?!
+                self.takes_arg[long] = False
+
+            # If this is an alias option, make sure its "takes arg" flag is
+            # the same as the option it's aliased to.
+            alias_to = self.alias.get(long)
+            if alias_to is not None:
+                if self.takes_arg[long] != self.takes_arg[alias_to]:
+                    raise DistutilsGetoptError(
+                        f"invalid alias '{long}': inconsistent with "
+                        f"aliased option '{alias_to}' (one of them takes a value, "
+                        "the other doesn't"
+                    )
+
+            # Now enforce some bondage on the long option name, so we can
+            # later translate it to an attribute name on some object.  Have
+            # to do this a bit late to make sure we've removed any trailing
+            # '='.
+            if not longopt_re.match(long):
+                raise DistutilsGetoptError(
+                    f"invalid long option name '{long}' "
+                    "(must be letters, numbers, hyphens only"
+                )
+
+            self.attr_name[long] = self.get_attr_name(long)
+            if short:
+                self.short_opts.append(short)
+                self.short2long[short[0]] = long
+
+    def getopt(self, args: Sequence[str] | None = None, object=None):  # noqa: C901
+        """Parse command-line options in args. Store as attributes on object.
+
+        If 'args' is None or not supplied, uses 'sys.argv[1:]'.  If
+        'object' is None or not supplied, creates a new OptionDummy
+        object, stores option values there, and returns a tuple (args,
+        object).  If 'object' is supplied, it is modified in place and
+        'getopt()' just returns 'args'; in both cases, the returned
+        'args' is a modified copy of the passed-in 'args' list, which
+        is left untouched.
+        """
+        if args is None:
+            args = sys.argv[1:]
+        if object is None:
+            object = OptionDummy()
+            created_object = True
+        else:
+            created_object = False
+
+        self._grok_option_table()
+
+        short_opts = ' '.join(self.short_opts)
+        try:
+            opts, args = getopt.getopt(args, short_opts, self.long_opts)
+        except getopt.error as msg:
+            raise DistutilsArgError(msg)
+
+        for opt, val in opts:
+            if len(opt) == 2 and opt[0] == '-':  # it's a short option
+                opt = self.short2long[opt[1]]
+            else:
+                assert len(opt) > 2 and opt[:2] == '--'
+                opt = opt[2:]
+
+            alias = self.alias.get(opt)
+            if alias:
+                opt = alias
+
+            if not self.takes_arg[opt]:  # boolean option?
+                assert val == '', "boolean option can't have value"
+                alias = self.negative_alias.get(opt)
+                if alias:
+                    opt = alias
+                    val = 0
+                else:
+                    val = 1
+
+            attr = self.attr_name[opt]
+            # The only repeating option at the moment is 'verbose'.
+            # It has a negative option -q quiet, which should set verbose = False.
+            if val and self.repeat.get(attr) is not None:
+                val = getattr(object, attr, 0) + 1
+            setattr(object, attr, val)
+            self.option_order.append((opt, val))
+
+        # for opts
+        if created_object:
+            return args, object
+        else:
+            return args
+
+    def get_option_order(self):
+        """Returns the list of (option, value) tuples processed by the
+        previous run of 'getopt()'.  Raises RuntimeError if
+        'getopt()' hasn't been called yet.
+        """
+        if self.option_order is None:
+            raise RuntimeError("'getopt()' hasn't been called yet")
+        else:
+            return self.option_order
+
+    def generate_help(self, header=None):  # noqa: C901
+        """Generate help text (a list of strings, one per suggested line of
+        output) from the option table for this FancyGetopt object.
+        """
+        # Blithely assume the option table is good: probably wouldn't call
+        # 'generate_help()' unless you've already called 'getopt()'.
+
+        # First pass: determine maximum length of long option names
+        max_opt = 0
+        for option in self.option_table:
+            long = option[0]
+            short = option[1]
+            ell = len(long)
+            if long[-1] == '=':
+                ell = ell - 1
+            if short is not None:
+                ell = ell + 5  # " (-x)" where short == 'x'
+            if ell > max_opt:
+                max_opt = ell
+
+        opt_width = max_opt + 2 + 2 + 2  # room for indent + dashes + gutter
+
+        # Typical help block looks like this:
+        #   --foo       controls foonabulation
+        # Help block for longest option looks like this:
+        #   --flimflam  set the flim-flam level
+        # and with wrapped text:
+        #   --flimflam  set the flim-flam level (must be between
+        #               0 and 100, except on Tuesdays)
+        # Options with short names will have the short name shown (but
+        # it doesn't contribute to max_opt):
+        #   --foo (-f)  controls foonabulation
+        # If adding the short option would make the left column too wide,
+        # we push the explanation off to the next line
+        #   --flimflam (-l)
+        #               set the flim-flam level
+        # Important parameters:
+        #   - 2 spaces before option block start lines
+        #   - 2 dashes for each long option name
+        #   - min. 2 spaces between option and explanation (gutter)
+        #   - 5 characters (incl. space) for short option name
+
+        # Now generate lines of help text.  (If 80 columns were good enough
+        # for Jesus, then 78 columns are good enough for me!)
+        line_width = 78
+        text_width = line_width - opt_width
+        big_indent = ' ' * opt_width
+        if header:
+            lines = [header]
+        else:
+            lines = ['Option summary:']
+
+        for option in self.option_table:
+            long, short, help = option[:3]
+            text = wrap_text(help, text_width)
+            if long[-1] == '=':
+                long = long[0:-1]
+
+            # Case 1: no short option at all (makes life easy)
+            if short is None:
+                if text:
+                    lines.append(f"  --{long:<{max_opt}}  {text[0]}")
+                else:
+                    lines.append(f"  --{long:<{max_opt}}")
+
+            # Case 2: we have a short option, so we have to include it
+            # just after the long option
+            else:
+                opt_names = f"{long} (-{short})"
+                if text:
+                    lines.append(f"  --{opt_names:<{max_opt}}  {text[0]}")
+                else:
+                    lines.append(f"  --{opt_names:<{max_opt}}")
+
+            for ell in text[1:]:
+                lines.append(big_indent + ell)
+        return lines
+
+    def print_help(self, header=None, file=None):
+        if file is None:
+            file = sys.stdout
+        for line in self.generate_help(header):
+            file.write(line + "\n")
+
+
+def fancy_getopt(options, negative_opt, object, args: Sequence[str] | None):
+    parser = FancyGetopt(options)
+    parser.set_negative_aliases(negative_opt)
+    return parser.getopt(args, object)
+
+
+WS_TRANS = {ord(_wschar): ' ' for _wschar in string.whitespace}
+
+
+def wrap_text(text, width):
+    """wrap_text(text : string, width : int) -> [string]
+
+    Split 'text' into multiple lines of no more than 'width' characters
+    each, and return the list of strings that results.
+    """
+    if text is None:
+        return []
+    if len(text) <= width:
+        return [text]
+
+    text = text.expandtabs()
+    text = text.translate(WS_TRANS)
+    chunks = re.split(r'( +|-+)', text)
+    chunks = [ch for ch in chunks if ch]  # ' - ' results in empty strings
+    lines = []
+
+    while chunks:
+        cur_line = []  # list of chunks (to-be-joined)
+        cur_len = 0  # length of current line
+
+        while chunks:
+            ell = len(chunks[0])
+            if cur_len + ell <= width:  # can squeeze (at least) this chunk in
+                cur_line.append(chunks[0])
+                del chunks[0]
+                cur_len = cur_len + ell
+            else:  # this line is full
+                # drop last chunk if all space
+                if cur_line and cur_line[-1][0] == ' ':
+                    del cur_line[-1]
+                break
+
+        if chunks:  # any chunks left to process?
+            # if the current line is still empty, then we had a single
+            # chunk that's too big too fit on a line -- so we break
+            # down and break it up at the line width
+            if cur_len == 0:
+                cur_line.append(chunks[0][0:width])
+                chunks[0] = chunks[0][width:]
+
+            # all-whitespace chunks at the end of a line can be discarded
+            # (and we know from the re.split above that if a chunk has
+            # *any* whitespace, it is *all* whitespace)
+            if chunks[0][0] == ' ':
+                del chunks[0]
+
+        # and store this line in the list-of-all-lines -- as a single
+        # string, of course!
+        lines.append(''.join(cur_line))
+
+    return lines
+
+
+def translate_longopt(opt):
+    """Convert a long option name to a valid Python identifier by
+    changing "-" to "_".
+    """
+    return opt.translate(longopt_xlate)
+
+
+class OptionDummy:
+    """Dummy class just used as a place to hold command-line option
+    values as instance attributes."""
+
+    def __init__(self, options: Sequence[Any] = []):
+        """Create a new OptionDummy instance.  The attributes listed in
+        'options' will be initialized to None."""
+        for opt in options:
+            setattr(self, opt, None)
+
+
+if __name__ == "__main__":
+    text = """\
+Tra-la-la, supercalifragilisticexpialidocious.
+How *do* you spell that odd word, anyways?
+(Someone ask Mary -- she'll know [or she'll
+say, "How should I know?"].)"""
+
+    for w in (10, 20, 30, 40):
+        print(f"width: {w}")
+        print("\n".join(wrap_text(text, w)))
+        print()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/file_util.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/file_util.py
new file mode 100644
index 00000000..0acc8cb8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/file_util.py
@@ -0,0 +1,236 @@
+"""distutils.file_util
+
+Utility functions for operating on single files.
+"""
+
+import os
+
+from ._log import log
+from .errors import DistutilsFileError
+
+# for generating verbose output in 'copy_file()'
+_copy_action = {None: 'copying', 'hard': 'hard linking', 'sym': 'symbolically linking'}
+
+
+def _copy_file_contents(src, dst, buffer_size=16 * 1024):  # noqa: C901
+    """Copy the file 'src' to 'dst'; both must be filenames.  Any error
+    opening either file, reading from 'src', or writing to 'dst', raises
+    DistutilsFileError.  Data is read/written in chunks of 'buffer_size'
+    bytes (default 16k).  No attempt is made to handle anything apart from
+    regular files.
+    """
+    # Stolen from shutil module in the standard library, but with
+    # custom error-handling added.
+    fsrc = None
+    fdst = None
+    try:
+        try:
+            fsrc = open(src, 'rb')
+        except OSError as e:
+            raise DistutilsFileError(f"could not open '{src}': {e.strerror}")
+
+        if os.path.exists(dst):
+            try:
+                os.unlink(dst)
+            except OSError as e:
+                raise DistutilsFileError(f"could not delete '{dst}': {e.strerror}")
+
+        try:
+            fdst = open(dst, 'wb')
+        except OSError as e:
+            raise DistutilsFileError(f"could not create '{dst}': {e.strerror}")
+
+        while True:
+            try:
+                buf = fsrc.read(buffer_size)
+            except OSError as e:
+                raise DistutilsFileError(f"could not read from '{src}': {e.strerror}")
+
+            if not buf:
+                break
+
+            try:
+                fdst.write(buf)
+            except OSError as e:
+                raise DistutilsFileError(f"could not write to '{dst}': {e.strerror}")
+    finally:
+        if fdst:
+            fdst.close()
+        if fsrc:
+            fsrc.close()
+
+
+def copy_file(  # noqa: C901
+    src,
+    dst,
+    preserve_mode=True,
+    preserve_times=True,
+    update=False,
+    link=None,
+    verbose=True,
+    dry_run=False,
+):
+    """Copy a file 'src' to 'dst'.  If 'dst' is a directory, then 'src' is
+    copied there with the same name; otherwise, it must be a filename.  (If
+    the file exists, it will be ruthlessly clobbered.)  If 'preserve_mode'
+    is true (the default), the file's mode (type and permission bits, or
+    whatever is analogous on the current platform) is copied.  If
+    'preserve_times' is true (the default), the last-modified and
+    last-access times are copied as well.  If 'update' is true, 'src' will
+    only be copied if 'dst' does not exist, or if 'dst' does exist but is
+    older than 'src'.
+
+    'link' allows you to make hard links (os.link) or symbolic links
+    (os.symlink) instead of copying: set it to "hard" or "sym"; if it is
+    None (the default), files are copied.  Don't set 'link' on systems that
+    don't support it: 'copy_file()' doesn't check if hard or symbolic
+    linking is available. If hardlink fails, falls back to
+    _copy_file_contents().
+
+    Under Mac OS, uses the native file copy function in macostools; on
+    other systems, uses '_copy_file_contents()' to copy file contents.
+
+    Return a tuple (dest_name, copied): 'dest_name' is the actual name of
+    the output file, and 'copied' is true if the file was copied (or would
+    have been copied, if 'dry_run' true).
+    """
+    # XXX if the destination file already exists, we clobber it if
+    # copying, but blow up if linking.  Hmmm.  And I don't know what
+    # macostools.copyfile() does.  Should definitely be consistent, and
+    # should probably blow up if destination exists and we would be
+    # changing it (ie. it's not already a hard/soft link to src OR
+    # (not update) and (src newer than dst).
+
+    from distutils._modified import newer
+    from stat import S_IMODE, ST_ATIME, ST_MODE, ST_MTIME
+
+    if not os.path.isfile(src):
+        raise DistutilsFileError(
+            f"can't copy '{src}': doesn't exist or not a regular file"
+        )
+
+    if os.path.isdir(dst):
+        dir = dst
+        dst = os.path.join(dst, os.path.basename(src))
+    else:
+        dir = os.path.dirname(dst)
+
+    if update and not newer(src, dst):
+        if verbose >= 1:
+            log.debug("not copying %s (output up-to-date)", src)
+        return (dst, False)
+
+    try:
+        action = _copy_action[link]
+    except KeyError:
+        raise ValueError(f"invalid value '{link}' for 'link' argument")
+
+    if verbose >= 1:
+        if os.path.basename(dst) == os.path.basename(src):
+            log.info("%s %s -> %s", action, src, dir)
+        else:
+            log.info("%s %s -> %s", action, src, dst)
+
+    if dry_run:
+        return (dst, True)
+
+    # If linking (hard or symbolic), use the appropriate system call
+    # (Unix only, of course, but that's the caller's responsibility)
+    elif link == 'hard':
+        if not (os.path.exists(dst) and os.path.samefile(src, dst)):
+            try:
+                os.link(src, dst)
+            except OSError:
+                # If hard linking fails, fall back on copying file
+                # (some special filesystems don't support hard linking
+                #  even under Unix, see issue #8876).
+                pass
+            else:
+                return (dst, True)
+    elif link == 'sym':
+        if not (os.path.exists(dst) and os.path.samefile(src, dst)):
+            os.symlink(src, dst)
+            return (dst, True)
+
+    # Otherwise (non-Mac, not linking), copy the file contents and
+    # (optionally) copy the times and mode.
+    _copy_file_contents(src, dst)
+    if preserve_mode or preserve_times:
+        st = os.stat(src)
+
+        # According to David Ascher <da@ski.org>, utime() should be done
+        # before chmod() (at least under NT).
+        if preserve_times:
+            os.utime(dst, (st[ST_ATIME], st[ST_MTIME]))
+        if preserve_mode:
+            os.chmod(dst, S_IMODE(st[ST_MODE]))
+
+    return (dst, True)
+
+
+# XXX I suspect this is Unix-specific -- need porting help!
+def move_file(src, dst, verbose=True, dry_run=False):  # noqa: C901
+    """Move a file 'src' to 'dst'.  If 'dst' is a directory, the file will
+    be moved into it with the same name; otherwise, 'src' is just renamed
+    to 'dst'.  Return the new full name of the file.
+
+    Handles cross-device moves on Unix using 'copy_file()'.  What about
+    other systems???
+    """
+    import errno
+    from os.path import basename, dirname, exists, isdir, isfile
+
+    if verbose >= 1:
+        log.info("moving %s -> %s", src, dst)
+
+    if dry_run:
+        return dst
+
+    if not isfile(src):
+        raise DistutilsFileError(f"can't move '{src}': not a regular file")
+
+    if isdir(dst):
+        dst = os.path.join(dst, basename(src))
+    elif exists(dst):
+        raise DistutilsFileError(
+            f"can't move '{src}': destination '{dst}' already exists"
+        )
+
+    if not isdir(dirname(dst)):
+        raise DistutilsFileError(
+            f"can't move '{src}': destination '{dst}' not a valid path"
+        )
+
+    copy_it = False
+    try:
+        os.rename(src, dst)
+    except OSError as e:
+        (num, msg) = e.args
+        if num == errno.EXDEV:
+            copy_it = True
+        else:
+            raise DistutilsFileError(f"couldn't move '{src}' to '{dst}': {msg}")
+
+    if copy_it:
+        copy_file(src, dst, verbose=verbose)
+        try:
+            os.unlink(src)
+        except OSError as e:
+            (num, msg) = e.args
+            try:
+                os.unlink(dst)
+            except OSError:
+                pass
+            raise DistutilsFileError(
+                f"couldn't move '{src}' to '{dst}' by copy/delete: "
+                f"delete '{src}' failed: {msg}"
+            )
+    return dst
+
+
+def write_file(filename, contents):
+    """Create a file with the specified name and write 'contents' (a
+    sequence of strings without line terminators) to it.
+    """
+    with open(filename, 'w', encoding='utf-8') as f:
+        f.writelines(line + '\n' for line in contents)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/filelist.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/filelist.py
new file mode 100644
index 00000000..70dc0fde
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/filelist.py
@@ -0,0 +1,431 @@
+"""distutils.filelist
+
+Provides the FileList class, used for poking about the filesystem
+and building lists of files.
+"""
+
+from __future__ import annotations
+
+import fnmatch
+import functools
+import os
+import re
+from collections.abc import Iterable
+from typing import Literal, overload
+
+from ._log import log
+from .errors import DistutilsInternalError, DistutilsTemplateError
+from .util import convert_path
+
+
+class FileList:
+    """A list of files built by on exploring the filesystem and filtered by
+    applying various patterns to what we find there.
+
+    Instance attributes:
+      dir
+        directory from which files will be taken -- only used if
+        'allfiles' not supplied to constructor
+      files
+        list of filenames currently being built/filtered/manipulated
+      allfiles
+        complete list of files under consideration (ie. without any
+        filtering applied)
+    """
+
+    def __init__(self, warn: object = None, debug_print: object = None) -> None:
+        # ignore argument to FileList, but keep them for backwards
+        # compatibility
+        self.allfiles: Iterable[str] | None = None
+        self.files: list[str] = []
+
+    def set_allfiles(self, allfiles: Iterable[str]) -> None:
+        self.allfiles = allfiles
+
+    def findall(self, dir: str | os.PathLike[str] = os.curdir) -> None:
+        self.allfiles = findall(dir)
+
+    def debug_print(self, msg: object) -> None:
+        """Print 'msg' to stdout if the global DEBUG (taken from the
+        DISTUTILS_DEBUG environment variable) flag is true.
+        """
+        from distutils.debug import DEBUG
+
+        if DEBUG:
+            print(msg)
+
+    # Collection methods
+
+    def append(self, item: str) -> None:
+        self.files.append(item)
+
+    def extend(self, items: Iterable[str]) -> None:
+        self.files.extend(items)
+
+    def sort(self) -> None:
+        # Not a strict lexical sort!
+        sortable_files = sorted(map(os.path.split, self.files))
+        self.files = []
+        for sort_tuple in sortable_files:
+            self.files.append(os.path.join(*sort_tuple))
+
+    # Other miscellaneous utility methods
+
+    def remove_duplicates(self) -> None:
+        # Assumes list has been sorted!
+        for i in range(len(self.files) - 1, 0, -1):
+            if self.files[i] == self.files[i - 1]:
+                del self.files[i]
+
+    # "File template" methods
+
+    def _parse_template_line(self, line):
+        words = line.split()
+        action = words[0]
+
+        patterns = dir = dir_pattern = None
+
+        if action in ('include', 'exclude', 'global-include', 'global-exclude'):
+            if len(words) < 2:
+                raise DistutilsTemplateError(
+                    f"'{action}' expects <pattern1> <pattern2> ..."
+                )
+            patterns = [convert_path(w) for w in words[1:]]
+        elif action in ('recursive-include', 'recursive-exclude'):
+            if len(words) < 3:
+                raise DistutilsTemplateError(
+                    f"'{action}' expects <dir> <pattern1> <pattern2> ..."
+                )
+            dir = convert_path(words[1])
+            patterns = [convert_path(w) for w in words[2:]]
+        elif action in ('graft', 'prune'):
+            if len(words) != 2:
+                raise DistutilsTemplateError(
+                    f"'{action}' expects a single <dir_pattern>"
+                )
+            dir_pattern = convert_path(words[1])
+        else:
+            raise DistutilsTemplateError(f"unknown action '{action}'")
+
+        return (action, patterns, dir, dir_pattern)
+
+    def process_template_line(self, line: str) -> None:  # noqa: C901
+        # Parse the line: split it up, make sure the right number of words
+        # is there, and return the relevant words.  'action' is always
+        # defined: it's the first word of the line.  Which of the other
+        # three are defined depends on the action; it'll be either
+        # patterns, (dir and patterns), or (dir_pattern).
+        (action, patterns, dir, dir_pattern) = self._parse_template_line(line)
+
+        # OK, now we know that the action is valid and we have the
+        # right number of words on the line for that action -- so we
+        # can proceed with minimal error-checking.
+        if action == 'include':
+            self.debug_print("include " + ' '.join(patterns))
+            for pattern in patterns:
+                if not self.include_pattern(pattern, anchor=True):
+                    log.warning("warning: no files found matching '%s'", pattern)
+
+        elif action == 'exclude':
+            self.debug_print("exclude " + ' '.join(patterns))
+            for pattern in patterns:
+                if not self.exclude_pattern(pattern, anchor=True):
+                    log.warning(
+                        "warning: no previously-included files found matching '%s'",
+                        pattern,
+                    )
+
+        elif action == 'global-include':
+            self.debug_print("global-include " + ' '.join(patterns))
+            for pattern in patterns:
+                if not self.include_pattern(pattern, anchor=False):
+                    log.warning(
+                        (
+                            "warning: no files found matching '%s' "
+                            "anywhere in distribution"
+                        ),
+                        pattern,
+                    )
+
+        elif action == 'global-exclude':
+            self.debug_print("global-exclude " + ' '.join(patterns))
+            for pattern in patterns:
+                if not self.exclude_pattern(pattern, anchor=False):
+                    log.warning(
+                        (
+                            "warning: no previously-included files matching "
+                            "'%s' found anywhere in distribution"
+                        ),
+                        pattern,
+                    )
+
+        elif action == 'recursive-include':
+            self.debug_print("recursive-include {} {}".format(dir, ' '.join(patterns)))
+            for pattern in patterns:
+                if not self.include_pattern(pattern, prefix=dir):
+                    msg = "warning: no files found matching '%s' under directory '%s'"
+                    log.warning(msg, pattern, dir)
+
+        elif action == 'recursive-exclude':
+            self.debug_print("recursive-exclude {} {}".format(dir, ' '.join(patterns)))
+            for pattern in patterns:
+                if not self.exclude_pattern(pattern, prefix=dir):
+                    log.warning(
+                        (
+                            "warning: no previously-included files matching "
+                            "'%s' found under directory '%s'"
+                        ),
+                        pattern,
+                        dir,
+                    )
+
+        elif action == 'graft':
+            self.debug_print("graft " + dir_pattern)
+            if not self.include_pattern(None, prefix=dir_pattern):
+                log.warning("warning: no directories found matching '%s'", dir_pattern)
+
+        elif action == 'prune':
+            self.debug_print("prune " + dir_pattern)
+            if not self.exclude_pattern(None, prefix=dir_pattern):
+                log.warning(
+                    ("no previously-included directories found matching '%s'"),
+                    dir_pattern,
+                )
+        else:
+            raise DistutilsInternalError(
+                f"this cannot happen: invalid action '{action}'"
+            )
+
+    # Filtering/selection methods
+    @overload
+    def include_pattern(
+        self,
+        pattern: str,
+        anchor: bool = True,
+        prefix: str | None = None,
+        is_regex: Literal[False] = False,
+    ) -> bool: ...
+    @overload
+    def include_pattern(
+        self,
+        pattern: str | re.Pattern[str],
+        anchor: bool = True,
+        prefix: str | None = None,
+        *,
+        is_regex: Literal[True],
+    ) -> bool: ...
+    @overload
+    def include_pattern(
+        self,
+        pattern: str | re.Pattern[str],
+        anchor: bool,
+        prefix: str | None,
+        is_regex: Literal[True],
+    ) -> bool: ...
+    def include_pattern(
+        self,
+        pattern: str | re.Pattern,
+        anchor: bool = True,
+        prefix: str | None = None,
+        is_regex: bool = False,
+    ) -> bool:
+        """Select strings (presumably filenames) from 'self.files' that
+        match 'pattern', a Unix-style wildcard (glob) pattern.  Patterns
+        are not quite the same as implemented by the 'fnmatch' module: '*'
+        and '?'  match non-special characters, where "special" is platform-
+        dependent: slash on Unix; colon, slash, and backslash on
+        DOS/Windows; and colon on Mac OS.
+
+        If 'anchor' is true (the default), then the pattern match is more
+        stringent: "*.py" will match "foo.py" but not "foo/bar.py".  If
+        'anchor' is false, both of these will match.
+
+        If 'prefix' is supplied, then only filenames starting with 'prefix'
+        (itself a pattern) and ending with 'pattern', with anything in between
+        them, will match.  'anchor' is ignored in this case.
+
+        If 'is_regex' is true, 'anchor' and 'prefix' are ignored, and
+        'pattern' is assumed to be either a string containing a regex or a
+        regex object -- no translation is done, the regex is just compiled
+        and used as-is.
+
+        Selected strings will be added to self.files.
+
+        Return True if files are found, False otherwise.
+        """
+        # XXX docstring lying about what the special chars are?
+        files_found = False
+        pattern_re = translate_pattern(pattern, anchor, prefix, is_regex)
+        self.debug_print(f"include_pattern: applying regex r'{pattern_re.pattern}'")
+
+        # delayed loading of allfiles list
+        if self.allfiles is None:
+            self.findall()
+
+        for name in self.allfiles:
+            if pattern_re.search(name):
+                self.debug_print(" adding " + name)
+                self.files.append(name)
+                files_found = True
+        return files_found
+
+    @overload
+    def exclude_pattern(
+        self,
+        pattern: str,
+        anchor: bool = True,
+        prefix: str | None = None,
+        is_regex: Literal[False] = False,
+    ) -> bool: ...
+    @overload
+    def exclude_pattern(
+        self,
+        pattern: str | re.Pattern[str],
+        anchor: bool = True,
+        prefix: str | None = None,
+        *,
+        is_regex: Literal[True],
+    ) -> bool: ...
+    @overload
+    def exclude_pattern(
+        self,
+        pattern: str | re.Pattern[str],
+        anchor: bool,
+        prefix: str | None,
+        is_regex: Literal[True],
+    ) -> bool: ...
+    def exclude_pattern(
+        self,
+        pattern: str | re.Pattern,
+        anchor: bool = True,
+        prefix: str | None = None,
+        is_regex: bool = False,
+    ) -> bool:
+        """Remove strings (presumably filenames) from 'files' that match
+        'pattern'.  Other parameters are the same as for
+        'include_pattern()', above.
+        The list 'self.files' is modified in place.
+        Return True if files are found, False otherwise.
+        """
+        files_found = False
+        pattern_re = translate_pattern(pattern, anchor, prefix, is_regex)
+        self.debug_print(f"exclude_pattern: applying regex r'{pattern_re.pattern}'")
+        for i in range(len(self.files) - 1, -1, -1):
+            if pattern_re.search(self.files[i]):
+                self.debug_print(" removing " + self.files[i])
+                del self.files[i]
+                files_found = True
+        return files_found
+
+
+# Utility functions
+
+
+def _find_all_simple(path):
+    """
+    Find all files under 'path'
+    """
+    all_unique = _UniqueDirs.filter(os.walk(path, followlinks=True))
+    results = (
+        os.path.join(base, file) for base, dirs, files in all_unique for file in files
+    )
+    return filter(os.path.isfile, results)
+
+
+class _UniqueDirs(set):
+    """
+    Exclude previously-seen dirs from walk results,
+    avoiding infinite recursion.
+    Ref https://bugs.python.org/issue44497.
+    """
+
+    def __call__(self, walk_item):
+        """
+        Given an item from an os.walk result, determine
+        if the item represents a unique dir for this instance
+        and if not, prevent further traversal.
+        """
+        base, dirs, files = walk_item
+        stat = os.stat(base)
+        candidate = stat.st_dev, stat.st_ino
+        found = candidate in self
+        if found:
+            del dirs[:]
+        self.add(candidate)
+        return not found
+
+    @classmethod
+    def filter(cls, items):
+        return filter(cls(), items)
+
+
+def findall(dir: str | os.PathLike[str] = os.curdir):
+    """
+    Find all files under 'dir' and return the list of full filenames.
+    Unless dir is '.', return full filenames with dir prepended.
+    """
+    files = _find_all_simple(dir)
+    if dir == os.curdir:
+        make_rel = functools.partial(os.path.relpath, start=dir)
+        files = map(make_rel, files)
+    return list(files)
+
+
+def glob_to_re(pattern):
+    """Translate a shell-like glob pattern to a regular expression; return
+    a string containing the regex.  Differs from 'fnmatch.translate()' in
+    that '*' does not match "special characters" (which are
+    platform-specific).
+    """
+    pattern_re = fnmatch.translate(pattern)
+
+    # '?' and '*' in the glob pattern become '.' and '.*' in the RE, which
+    # IMHO is wrong -- '?' and '*' aren't supposed to match slash in Unix,
+    # and by extension they shouldn't match such "special characters" under
+    # any OS.  So change all non-escaped dots in the RE to match any
+    # character except the special characters (currently: just os.sep).
+    sep = os.sep
+    if os.sep == '\\':
+        # we're using a regex to manipulate a regex, so we need
+        # to escape the backslash twice
+        sep = r'\\\\'
+    escaped = rf'\1[^{sep}]'
+    pattern_re = re.sub(r'((?<!\\)(\\\\)*)\.', escaped, pattern_re)
+    return pattern_re
+
+
+def translate_pattern(pattern, anchor=True, prefix=None, is_regex=False):
+    """Translate a shell-like wildcard pattern to a compiled regular
+    expression.  Return the compiled regex.  If 'is_regex' true,
+    then 'pattern' is directly compiled to a regex (if it's a string)
+    or just returned as-is (assumes it's a regex object).
+    """
+    if is_regex:
+        if isinstance(pattern, str):
+            return re.compile(pattern)
+        else:
+            return pattern
+
+    # ditch start and end characters
+    start, _, end = glob_to_re('_').partition('_')
+
+    if pattern:
+        pattern_re = glob_to_re(pattern)
+        assert pattern_re.startswith(start) and pattern_re.endswith(end)
+    else:
+        pattern_re = ''
+
+    if prefix is not None:
+        prefix_re = glob_to_re(prefix)
+        assert prefix_re.startswith(start) and prefix_re.endswith(end)
+        prefix_re = prefix_re[len(start) : len(prefix_re) - len(end)]
+        sep = os.sep
+        if os.sep == '\\':
+            sep = r'\\'
+        pattern_re = pattern_re[len(start) : len(pattern_re) - len(end)]
+        pattern_re = rf'{start}\A{prefix_re}{sep}.*{pattern_re}{end}'
+    else:  # no prefix -- respect anchor flag
+        if anchor:
+            pattern_re = rf'{start}\A{pattern_re[len(start) :]}'
+
+    return re.compile(pattern_re)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/log.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/log.py
new file mode 100644
index 00000000..8abb09cf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/log.py
@@ -0,0 +1,56 @@
+"""
+A simple log mechanism styled after PEP 282.
+
+Retained for compatibility and should not be used.
+"""
+
+import logging
+import warnings
+
+from ._log import log as _global_log
+
+DEBUG = logging.DEBUG
+INFO = logging.INFO
+WARN = logging.WARN
+ERROR = logging.ERROR
+FATAL = logging.FATAL
+
+log = _global_log.log
+debug = _global_log.debug
+info = _global_log.info
+warn = _global_log.warning
+error = _global_log.error
+fatal = _global_log.fatal
+
+
+def set_threshold(level):
+    orig = _global_log.level
+    _global_log.setLevel(level)
+    return orig
+
+
+def set_verbosity(v):
+    if v <= 0:
+        set_threshold(logging.WARN)
+    elif v == 1:
+        set_threshold(logging.INFO)
+    elif v >= 2:
+        set_threshold(logging.DEBUG)
+
+
+class Log(logging.Logger):
+    """distutils.log.Log is deprecated, please use an alternative from `logging`."""
+
+    def __init__(self, threshold=WARN):
+        warnings.warn(Log.__doc__)  # avoid DeprecationWarning to ensure warn is shown
+        super().__init__(__name__, level=threshold)
+
+    @property
+    def threshold(self):
+        return self.level
+
+    @threshold.setter
+    def threshold(self, level):
+        self.setLevel(level)
+
+    warn = logging.Logger.warning
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/spawn.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/spawn.py
new file mode 100644
index 00000000..973668f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/spawn.py
@@ -0,0 +1,134 @@
+"""distutils.spawn
+
+Provides the 'spawn()' function, a front-end to various platform-
+specific functions for launching another program in a sub-process.
+"""
+
+from __future__ import annotations
+
+import os
+import platform
+import shutil
+import subprocess
+import sys
+import warnings
+from collections.abc import Mapping, MutableSequence
+from typing import TYPE_CHECKING, TypeVar, overload
+
+from ._log import log
+from .debug import DEBUG
+from .errors import DistutilsExecError
+
+if TYPE_CHECKING:
+    from subprocess import _ENV
+
+
+_MappingT = TypeVar("_MappingT", bound=Mapping)
+
+
+def _debug(cmd):
+    """
+    Render a subprocess command differently depending on DEBUG.
+    """
+    return cmd if DEBUG else cmd[0]
+
+
+def _inject_macos_ver(env: _MappingT | None) -> _MappingT | dict[str, str | int] | None:
+    if platform.system() != 'Darwin':
+        return env
+
+    from .util import MACOSX_VERSION_VAR, get_macosx_target_ver
+
+    target_ver = get_macosx_target_ver()
+    update = {MACOSX_VERSION_VAR: target_ver} if target_ver else {}
+    return {**_resolve(env), **update}
+
+
+@overload
+def _resolve(env: None) -> os._Environ[str]: ...
+@overload
+def _resolve(env: _MappingT) -> _MappingT: ...
+def _resolve(env: _MappingT | None) -> _MappingT | os._Environ[str]:
+    return os.environ if env is None else env
+
+
+def spawn(
+    cmd: MutableSequence[bytes | str | os.PathLike[str]],
+    search_path: bool = True,
+    verbose: bool = False,
+    dry_run: bool = False,
+    env: _ENV | None = None,
+) -> None:
+    """Run another program, specified as a command list 'cmd', in a new process.
+
+    'cmd' is just the argument list for the new process, ie.
+    cmd[0] is the program to run and cmd[1:] are the rest of its arguments.
+    There is no way to run a program with a name different from that of its
+    executable.
+
+    If 'search_path' is true (the default), the system's executable
+    search path will be used to find the program; otherwise, cmd[0]
+    must be the exact path to the executable.  If 'dry_run' is true,
+    the command will not actually be run.
+
+    Raise DistutilsExecError if running the program fails in any way; just
+    return on success.
+    """
+    log.info(subprocess.list2cmdline(cmd))
+    if dry_run:
+        return
+
+    if search_path:
+        executable = shutil.which(cmd[0])
+        if executable is not None:
+            cmd[0] = executable
+
+    try:
+        subprocess.check_call(cmd, env=_inject_macos_ver(env))
+    except OSError as exc:
+        raise DistutilsExecError(
+            f"command {_debug(cmd)!r} failed: {exc.args[-1]}"
+        ) from exc
+    except subprocess.CalledProcessError as err:
+        raise DistutilsExecError(
+            f"command {_debug(cmd)!r} failed with exit code {err.returncode}"
+        ) from err
+
+
+def find_executable(executable: str, path: str | None = None) -> str | None:
+    """Tries to find 'executable' in the directories listed in 'path'.
+
+    A string listing directories separated by 'os.pathsep'; defaults to
+    os.environ['PATH'].  Returns the complete filename or None if not found.
+    """
+    warnings.warn(
+        'Use shutil.which instead of find_executable', DeprecationWarning, stacklevel=2
+    )
+    _, ext = os.path.splitext(executable)
+    if (sys.platform == 'win32') and (ext != '.exe'):
+        executable = executable + '.exe'
+
+    if os.path.isfile(executable):
+        return executable
+
+    if path is None:
+        path = os.environ.get('PATH', None)
+        # bpo-35755: Don't fall through if PATH is the empty string
+        if path is None:
+            try:
+                path = os.confstr("CS_PATH")
+            except (AttributeError, ValueError):
+                # os.confstr() or CS_PATH is not available
+                path = os.defpath
+
+    # PATH='' doesn't match, whereas PATH=':' looks in the current directory
+    if not path:
+        return None
+
+    paths = path.split(os.pathsep)
+    for p in paths:
+        f = os.path.join(p, executable)
+        if os.path.isfile(f):
+            # the file exists, we have a shot at spawn working
+            return f
+    return None
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/sysconfig.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/sysconfig.py
new file mode 100644
index 00000000..7ddc869a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/sysconfig.py
@@ -0,0 +1,598 @@
+"""Provide access to Python's configuration information.  The specific
+configuration variables available depend heavily on the platform and
+configuration.  The values may be retrieved using
+get_config_var(name), and the list of variables is available via
+get_config_vars().keys().  Additional convenience functions are also
+available.
+
+Written by:   Fred L. Drake, Jr.
+Email:        <fdrake@acm.org>
+"""
+
+from __future__ import annotations
+
+import functools
+import os
+import pathlib
+import re
+import sys
+import sysconfig
+from typing import TYPE_CHECKING, Literal, overload
+
+from jaraco.functools import pass_none
+
+from .ccompiler import CCompiler
+from .compat import py39
+from .errors import DistutilsPlatformError
+from .util import is_mingw
+
+if TYPE_CHECKING:
+    from typing_extensions import deprecated
+else:
+
+    def deprecated(message):
+        return lambda fn: fn
+
+
+IS_PYPY = '__pypy__' in sys.builtin_module_names
+
+# These are needed in a couple of spots, so just compute them once.
+PREFIX = os.path.normpath(sys.prefix)
+EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
+BASE_PREFIX = os.path.normpath(sys.base_prefix)
+BASE_EXEC_PREFIX = os.path.normpath(sys.base_exec_prefix)
+
+# Path to the base directory of the project. On Windows the binary may
+# live in project/PCbuild/win32 or project/PCbuild/amd64.
+# set for cross builds
+if "_PYTHON_PROJECT_BASE" in os.environ:
+    project_base = os.path.abspath(os.environ["_PYTHON_PROJECT_BASE"])
+else:
+    if sys.executable:
+        project_base = os.path.dirname(os.path.abspath(sys.executable))
+    else:
+        # sys.executable can be empty if argv[0] has been changed and Python is
+        # unable to retrieve the real program name
+        project_base = os.getcwd()
+
+
+def _is_python_source_dir(d):
+    """
+    Return True if the target directory appears to point to an
+    un-installed Python.
+    """
+    modules = pathlib.Path(d).joinpath('Modules')
+    return any(modules.joinpath(fn).is_file() for fn in ('Setup', 'Setup.local'))
+
+
+_sys_home = getattr(sys, '_home', None)
+
+
+def _is_parent(dir_a, dir_b):
+    """
+    Return True if a is a parent of b.
+    """
+    return os.path.normcase(dir_a).startswith(os.path.normcase(dir_b))
+
+
+if os.name == 'nt':
+
+    @pass_none
+    def _fix_pcbuild(d):
+        # In a venv, sys._home will be inside BASE_PREFIX rather than PREFIX.
+        prefixes = PREFIX, BASE_PREFIX
+        matched = (
+            prefix
+            for prefix in prefixes
+            if _is_parent(d, os.path.join(prefix, "PCbuild"))
+        )
+        return next(matched, d)
+
+    project_base = _fix_pcbuild(project_base)
+    _sys_home = _fix_pcbuild(_sys_home)
+
+
+def _python_build():
+    if _sys_home:
+        return _is_python_source_dir(_sys_home)
+    return _is_python_source_dir(project_base)
+
+
+python_build = _python_build()
+
+
+# Calculate the build qualifier flags if they are defined.  Adding the flags
+# to the include and lib directories only makes sense for an installation, not
+# an in-source build.
+build_flags = ''
+try:
+    if not python_build:
+        build_flags = sys.abiflags
+except AttributeError:
+    # It's not a configure-based build, so the sys module doesn't have
+    # this attribute, which is fine.
+    pass
+
+
+def get_python_version():
+    """Return a string containing the major and minor Python version,
+    leaving off the patchlevel.  Sample return values could be '1.5'
+    or '2.2'.
+    """
+    return f'{sys.version_info.major}.{sys.version_info.minor}'
+
+
+def get_python_inc(plat_specific: bool = False, prefix: str | None = None) -> str:
+    """Return the directory containing installed Python header files.
+
+    If 'plat_specific' is false (the default), this is the path to the
+    non-platform-specific header files, i.e. Python.h and so on;
+    otherwise, this is the path to platform-specific header files
+    (namely pyconfig.h).
+
+    If 'prefix' is supplied, use it instead of sys.base_prefix or
+    sys.base_exec_prefix -- i.e., ignore 'plat_specific'.
+    """
+    default_prefix = BASE_EXEC_PREFIX if plat_specific else BASE_PREFIX
+    resolved_prefix = prefix if prefix is not None else default_prefix
+    # MinGW imitates posix like layout, but os.name != posix
+    os_name = "posix" if is_mingw() else os.name
+    try:
+        getter = globals()[f'_get_python_inc_{os_name}']
+    except KeyError:
+        raise DistutilsPlatformError(
+            "I don't know where Python installs its C header files "
+            f"on platform '{os.name}'"
+        )
+    return getter(resolved_prefix, prefix, plat_specific)
+
+
+@pass_none
+def _extant(path):
+    """
+    Replace path with None if it doesn't exist.
+    """
+    return path if os.path.exists(path) else None
+
+
+def _get_python_inc_posix(prefix, spec_prefix, plat_specific):
+    return (
+        _get_python_inc_posix_python(plat_specific)
+        or _extant(_get_python_inc_from_config(plat_specific, spec_prefix))
+        or _get_python_inc_posix_prefix(prefix)
+    )
+
+
+def _get_python_inc_posix_python(plat_specific):
+    """
+    Assume the executable is in the build directory. The
+    pyconfig.h file should be in the same directory. Since
+    the build directory may not be the source directory,
+    use "srcdir" from the makefile to find the "Include"
+    directory.
+    """
+    if not python_build:
+        return
+    if plat_specific:
+        return _sys_home or project_base
+    incdir = os.path.join(get_config_var('srcdir'), 'Include')
+    return os.path.normpath(incdir)
+
+
+def _get_python_inc_from_config(plat_specific, spec_prefix):
+    """
+    If no prefix was explicitly specified, provide the include
+    directory from the config vars. Useful when
+    cross-compiling, since the config vars may come from
+    the host
+    platform Python installation, while the current Python
+    executable is from the build platform installation.
+
+    >>> monkeypatch = getfixture('monkeypatch')
+    >>> gpifc = _get_python_inc_from_config
+    >>> monkeypatch.setitem(gpifc.__globals__, 'get_config_var', str.lower)
+    >>> gpifc(False, '/usr/bin/')
+    >>> gpifc(False, '')
+    >>> gpifc(False, None)
+    'includepy'
+    >>> gpifc(True, None)
+    'confincludepy'
+    """
+    if spec_prefix is None:
+        return get_config_var('CONF' * plat_specific + 'INCLUDEPY')
+
+
+def _get_python_inc_posix_prefix(prefix):
+    implementation = 'pypy' if IS_PYPY else 'python'
+    python_dir = implementation + get_python_version() + build_flags
+    return os.path.join(prefix, "include", python_dir)
+
+
+def _get_python_inc_nt(prefix, spec_prefix, plat_specific):
+    if python_build:
+        # Include both include dirs to ensure we can find pyconfig.h
+        return (
+            os.path.join(prefix, "include")
+            + os.path.pathsep
+            + os.path.dirname(sysconfig.get_config_h_filename())
+        )
+    return os.path.join(prefix, "include")
+
+
+# allow this behavior to be monkey-patched. Ref pypa/distutils#2.
+def _posix_lib(standard_lib, libpython, early_prefix, prefix):
+    if standard_lib:
+        return libpython
+    else:
+        return os.path.join(libpython, "site-packages")
+
+
+def get_python_lib(
+    plat_specific: bool = False, standard_lib: bool = False, prefix: str | None = None
+) -> str:
+    """Return the directory containing the Python library (standard or
+    site additions).
+
+    If 'plat_specific' is true, return the directory containing
+    platform-specific modules, i.e. any module from a non-pure-Python
+    module distribution; otherwise, return the platform-shared library
+    directory.  If 'standard_lib' is true, return the directory
+    containing standard Python library modules; otherwise, return the
+    directory for site-specific modules.
+
+    If 'prefix' is supplied, use it instead of sys.base_prefix or
+    sys.base_exec_prefix -- i.e., ignore 'plat_specific'.
+    """
+
+    early_prefix = prefix
+
+    if prefix is None:
+        if standard_lib:
+            prefix = plat_specific and BASE_EXEC_PREFIX or BASE_PREFIX
+        else:
+            prefix = plat_specific and EXEC_PREFIX or PREFIX
+
+    if os.name == "posix" or is_mingw():
+        if plat_specific or standard_lib:
+            # Platform-specific modules (any module from a non-pure-Python
+            # module distribution) or standard Python library modules.
+            libdir = getattr(sys, "platlibdir", "lib")
+        else:
+            # Pure Python
+            libdir = "lib"
+        implementation = 'pypy' if IS_PYPY else 'python'
+        libpython = os.path.join(prefix, libdir, implementation + get_python_version())
+        return _posix_lib(standard_lib, libpython, early_prefix, prefix)
+    elif os.name == "nt":
+        if standard_lib:
+            return os.path.join(prefix, "Lib")
+        else:
+            return os.path.join(prefix, "Lib", "site-packages")
+    else:
+        raise DistutilsPlatformError(
+            f"I don't know where Python installs its library on platform '{os.name}'"
+        )
+
+
+@functools.lru_cache
+def _customize_macos():
+    """
+    Perform first-time customization of compiler-related
+    config vars on macOS. Use after a compiler is known
+    to be needed. This customization exists primarily to support Pythons
+    from binary installers. The kind and paths to build tools on
+    the user system may vary significantly from the system
+    that Python itself was built on.  Also the user OS
+    version and build tools may not support the same set
+    of CPU architectures for universal builds.
+    """
+
+    sys.platform == "darwin" and __import__('_osx_support').customize_compiler(
+        get_config_vars()
+    )
+
+
+def customize_compiler(compiler: CCompiler) -> None:
+    """Do any platform-specific customization of a CCompiler instance.
+
+    Mainly needed on Unix, so we can plug in the information that
+    varies across Unices and is stored in Python's Makefile.
+    """
+    if compiler.compiler_type in ["unix", "cygwin"] or (
+        compiler.compiler_type == "mingw32" and is_mingw()
+    ):
+        _customize_macos()
+
+        (
+            cc,
+            cxx,
+            cflags,
+            ccshared,
+            ldshared,
+            ldcxxshared,
+            shlib_suffix,
+            ar,
+            ar_flags,
+        ) = get_config_vars(
+            'CC',
+            'CXX',
+            'CFLAGS',
+            'CCSHARED',
+            'LDSHARED',
+            'LDCXXSHARED',
+            'SHLIB_SUFFIX',
+            'AR',
+            'ARFLAGS',
+        )
+
+        cxxflags = cflags
+
+        if 'CC' in os.environ:
+            newcc = os.environ['CC']
+            if 'LDSHARED' not in os.environ and ldshared.startswith(cc):
+                # If CC is overridden, use that as the default
+                #       command for LDSHARED as well
+                ldshared = newcc + ldshared[len(cc) :]
+            cc = newcc
+        cxx = os.environ.get('CXX', cxx)
+        ldshared = os.environ.get('LDSHARED', ldshared)
+        ldcxxshared = os.environ.get('LDCXXSHARED', ldcxxshared)
+        cpp = os.environ.get(
+            'CPP',
+            cc + " -E",  # not always
+        )
+
+        ldshared = _add_flags(ldshared, 'LD')
+        ldcxxshared = _add_flags(ldcxxshared, 'LD')
+        cflags = os.environ.get('CFLAGS', cflags)
+        ldshared = _add_flags(ldshared, 'C')
+        cxxflags = os.environ.get('CXXFLAGS', cxxflags)
+        ldcxxshared = _add_flags(ldcxxshared, 'CXX')
+        cpp = _add_flags(cpp, 'CPP')
+        cflags = _add_flags(cflags, 'CPP')
+        cxxflags = _add_flags(cxxflags, 'CPP')
+        ldshared = _add_flags(ldshared, 'CPP')
+        ldcxxshared = _add_flags(ldcxxshared, 'CPP')
+
+        ar = os.environ.get('AR', ar)
+
+        archiver = ar + ' ' + os.environ.get('ARFLAGS', ar_flags)
+        cc_cmd = cc + ' ' + cflags
+        cxx_cmd = cxx + ' ' + cxxflags
+
+        compiler.set_executables(
+            preprocessor=cpp,
+            compiler=cc_cmd,
+            compiler_so=cc_cmd + ' ' + ccshared,
+            compiler_cxx=cxx_cmd,
+            compiler_so_cxx=cxx_cmd + ' ' + ccshared,
+            linker_so=ldshared,
+            linker_so_cxx=ldcxxshared,
+            linker_exe=cc,
+            linker_exe_cxx=cxx,
+            archiver=archiver,
+        )
+
+        if 'RANLIB' in os.environ and compiler.executables.get('ranlib', None):
+            compiler.set_executables(ranlib=os.environ['RANLIB'])
+
+        compiler.shared_lib_extension = shlib_suffix
+
+
+def get_config_h_filename() -> str:
+    """Return full pathname of installed pyconfig.h file."""
+    return sysconfig.get_config_h_filename()
+
+
+def get_makefile_filename() -> str:
+    """Return full pathname of installed Makefile from the Python build."""
+    return sysconfig.get_makefile_filename()
+
+
+def parse_config_h(fp, g=None):
+    """Parse a config.h-style file.
+
+    A dictionary containing name/value pairs is returned.  If an
+    optional dictionary is passed in as the second argument, it is
+    used instead of a new dictionary.
+    """
+    return sysconfig.parse_config_h(fp, vars=g)
+
+
+# Regexes needed for parsing Makefile (and similar syntaxes,
+# like old-style Setup files).
+_variable_rx = re.compile(r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)")
+_findvar1_rx = re.compile(r"\$\(([A-Za-z][A-Za-z0-9_]*)\)")
+_findvar2_rx = re.compile(r"\${([A-Za-z][A-Za-z0-9_]*)}")
+
+
+def parse_makefile(fn, g=None):  # noqa: C901
+    """Parse a Makefile-style file.
+
+    A dictionary containing name/value pairs is returned.  If an
+    optional dictionary is passed in as the second argument, it is
+    used instead of a new dictionary.
+    """
+    from distutils.text_file import TextFile
+
+    fp = TextFile(
+        fn,
+        strip_comments=True,
+        skip_blanks=True,
+        join_lines=True,
+        errors="surrogateescape",
+    )
+
+    if g is None:
+        g = {}
+    done = {}
+    notdone = {}
+
+    while True:
+        line = fp.readline()
+        if line is None:  # eof
+            break
+        m = _variable_rx.match(line)
+        if m:
+            n, v = m.group(1, 2)
+            v = v.strip()
+            # `$$' is a literal `$' in make
+            tmpv = v.replace('$$', '')
+
+            if "$" in tmpv:
+                notdone[n] = v
+            else:
+                try:
+                    v = int(v)
+                except ValueError:
+                    # insert literal `$'
+                    done[n] = v.replace('$$', '$')
+                else:
+                    done[n] = v
+
+    # Variables with a 'PY_' prefix in the makefile. These need to
+    # be made available without that prefix through sysconfig.
+    # Special care is needed to ensure that variable expansion works, even
+    # if the expansion uses the name without a prefix.
+    renamed_variables = ('CFLAGS', 'LDFLAGS', 'CPPFLAGS')
+
+    # do variable interpolation here
+    while notdone:
+        for name in list(notdone):
+            value = notdone[name]
+            m = _findvar1_rx.search(value) or _findvar2_rx.search(value)
+            if m:
+                n = m.group(1)
+                found = True
+                if n in done:
+                    item = str(done[n])
+                elif n in notdone:
+                    # get it on a subsequent round
+                    found = False
+                elif n in os.environ:
+                    # do it like make: fall back to environment
+                    item = os.environ[n]
+
+                elif n in renamed_variables:
+                    if name.startswith('PY_') and name[3:] in renamed_variables:
+                        item = ""
+
+                    elif 'PY_' + n in notdone:
+                        found = False
+
+                    else:
+                        item = str(done['PY_' + n])
+                else:
+                    done[n] = item = ""
+                if found:
+                    after = value[m.end() :]
+                    value = value[: m.start()] + item + after
+                    if "$" in after:
+                        notdone[name] = value
+                    else:
+                        try:
+                            value = int(value)
+                        except ValueError:
+                            done[name] = value.strip()
+                        else:
+                            done[name] = value
+                        del notdone[name]
+
+                        if name.startswith('PY_') and name[3:] in renamed_variables:
+                            name = name[3:]
+                            if name not in done:
+                                done[name] = value
+            else:
+                # bogus variable reference; just drop it since we can't deal
+                del notdone[name]
+
+    fp.close()
+
+    # strip spurious spaces
+    for k, v in done.items():
+        if isinstance(v, str):
+            done[k] = v.strip()
+
+    # save the results in the global dictionary
+    g.update(done)
+    return g
+
+
+def expand_makefile_vars(s, vars):
+    """Expand Makefile-style variables -- "${foo}" or "$(foo)" -- in
+    'string' according to 'vars' (a dictionary mapping variable names to
+    values).  Variables not present in 'vars' are silently expanded to the
+    empty string.  The variable values in 'vars' should not contain further
+    variable expansions; if 'vars' is the output of 'parse_makefile()',
+    you're fine.  Returns a variable-expanded version of 's'.
+    """
+
+    # This algorithm does multiple expansion, so if vars['foo'] contains
+    # "${bar}", it will expand ${foo} to ${bar}, and then expand
+    # ${bar}... and so forth.  This is fine as long as 'vars' comes from
+    # 'parse_makefile()', which takes care of such expansions eagerly,
+    # according to make's variable expansion semantics.
+
+    while True:
+        m = _findvar1_rx.search(s) or _findvar2_rx.search(s)
+        if m:
+            (beg, end) = m.span()
+            s = s[0:beg] + vars.get(m.group(1)) + s[end:]
+        else:
+            break
+    return s
+
+
+_config_vars = None
+
+
+@overload
+def get_config_vars() -> dict[str, str | int]: ...
+@overload
+def get_config_vars(arg: str, /, *args: str) -> list[str | int]: ...
+def get_config_vars(*args: str) -> list[str | int] | dict[str, str | int]:
+    """With no arguments, return a dictionary of all configuration
+    variables relevant for the current platform.  Generally this includes
+    everything needed to build extensions and install both pure modules and
+    extensions.  On Unix, this means every variable defined in Python's
+    installed Makefile; on Windows it's a much smaller set.
+
+    With arguments, return a list of values that result from looking up
+    each argument in the configuration variable dictionary.
+    """
+    global _config_vars
+    if _config_vars is None:
+        _config_vars = sysconfig.get_config_vars().copy()
+        py39.add_ext_suffix(_config_vars)
+
+    return [_config_vars.get(name) for name in args] if args else _config_vars
+
+
+@overload
+@deprecated(
+    "SO is deprecated, use EXT_SUFFIX. Support will be removed when this module is synchronized with stdlib Python 3.11"
+)
+def get_config_var(name: Literal["SO"]) -> int | str | None: ...
+@overload
+def get_config_var(name: str) -> int | str | None: ...
+def get_config_var(name: str) -> int | str | None:
+    """Return the value of a single variable using the dictionary
+    returned by 'get_config_vars()'.  Equivalent to
+    get_config_vars().get(name)
+    """
+    if name == 'SO':
+        import warnings
+
+        warnings.warn('SO is deprecated, use EXT_SUFFIX', DeprecationWarning, 2)
+    return get_config_vars().get(name)
+
+
+@pass_none
+def _add_flags(value: str, type: str) -> str:
+    """
+    Add any flags from the environment for the given type.
+
+    type is the prefix to FLAGS in the environment key (e.g. "C" for "CFLAGS").
+    """
+    flags = os.environ.get(f'{type}FLAGS')
+    return f'{value} {flags}' if flags else value
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/__init__.py
new file mode 100644
index 00000000..5a8ab061
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/__init__.py
@@ -0,0 +1,42 @@
+"""
+Test suite for distutils.
+
+Tests for the command classes in the distutils.command package are
+included in distutils.tests as well, instead of using a separate
+distutils.command.tests package, since command identification is done
+by import rather than matching pre-defined names.
+"""
+
+import shutil
+from collections.abc import Sequence
+
+
+def missing_compiler_executable(cmd_names: Sequence[str] = []):  # pragma: no cover
+    """Check if the compiler components used to build the interpreter exist.
+
+    Check for the existence of the compiler executables whose names are listed
+    in 'cmd_names' or all the compiler executables when 'cmd_names' is empty
+    and return the first missing executable or None when none is found
+    missing.
+
+    """
+    from distutils import ccompiler, errors, sysconfig
+
+    compiler = ccompiler.new_compiler()
+    sysconfig.customize_compiler(compiler)
+    if compiler.compiler_type == "msvc":
+        # MSVC has no executables, so check whether initialization succeeds
+        try:
+            compiler.initialize()
+        except errors.DistutilsPlatformError:
+            return "msvc"
+    for name in compiler.executables:
+        if cmd_names and name not in cmd_names:
+            continue
+        cmd = getattr(compiler, name)
+        if cmd_names:
+            assert cmd is not None, f"the '{name}' executable is not configured"
+        elif not cmd:
+            continue
+        if shutil.which(cmd[0]) is None:
+            return cmd[0]
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/compat/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/compat/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/compat/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/compat/py39.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/compat/py39.py
new file mode 100644
index 00000000..aca3939a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/compat/py39.py
@@ -0,0 +1,40 @@
+import sys
+
+if sys.version_info >= (3, 10):
+    from test.support.import_helper import (
+        CleanImport as CleanImport,
+    )
+    from test.support.import_helper import (
+        DirsOnSysPath as DirsOnSysPath,
+    )
+    from test.support.os_helper import (
+        EnvironmentVarGuard as EnvironmentVarGuard,
+    )
+    from test.support.os_helper import (
+        rmtree as rmtree,
+    )
+    from test.support.os_helper import (
+        skip_unless_symlink as skip_unless_symlink,
+    )
+    from test.support.os_helper import (
+        unlink as unlink,
+    )
+else:
+    from test.support import (
+        CleanImport as CleanImport,
+    )
+    from test.support import (
+        DirsOnSysPath as DirsOnSysPath,
+    )
+    from test.support import (
+        EnvironmentVarGuard as EnvironmentVarGuard,
+    )
+    from test.support import (
+        rmtree as rmtree,
+    )
+    from test.support import (
+        skip_unless_symlink as skip_unless_symlink,
+    )
+    from test.support import (
+        unlink as unlink,
+    )
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/support.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/support.py
new file mode 100644
index 00000000..9cd2b8a9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/support.py
@@ -0,0 +1,134 @@
+"""Support code for distutils test cases."""
+
+import itertools
+import os
+import pathlib
+import shutil
+import sys
+import sysconfig
+import tempfile
+from distutils.core import Distribution
+
+import pytest
+from more_itertools import always_iterable
+
+
+@pytest.mark.usefixtures('distutils_managed_tempdir')
+class TempdirManager:
+    """
+    Mix-in class that handles temporary directories for test cases.
+    """
+
+    def mkdtemp(self):
+        """Create a temporary directory that will be cleaned up.
+
+        Returns the path of the directory.
+        """
+        d = tempfile.mkdtemp()
+        self.tempdirs.append(d)
+        return d
+
+    def write_file(self, path, content='xxx'):
+        """Writes a file in the given path.
+
+        path can be a string or a sequence.
+        """
+        pathlib.Path(*always_iterable(path)).write_text(content, encoding='utf-8')
+
+    def create_dist(self, pkg_name='foo', **kw):
+        """Will generate a test environment.
+
+        This function creates:
+         - a Distribution instance using keywords
+         - a temporary directory with a package structure
+
+        It returns the package directory and the distribution
+        instance.
+        """
+        tmp_dir = self.mkdtemp()
+        pkg_dir = os.path.join(tmp_dir, pkg_name)
+        os.mkdir(pkg_dir)
+        dist = Distribution(attrs=kw)
+
+        return pkg_dir, dist
+
+
+class DummyCommand:
+    """Class to store options for retrieval via set_undefined_options()."""
+
+    def __init__(self, **kwargs):
+        vars(self).update(kwargs)
+
+    def ensure_finalized(self):
+        pass
+
+
+def copy_xxmodule_c(directory):
+    """Helper for tests that need the xxmodule.c source file.
+
+    Example use:
+
+        def test_compile(self):
+            copy_xxmodule_c(self.tmpdir)
+            self.assertIn('xxmodule.c', os.listdir(self.tmpdir))
+
+    If the source file can be found, it will be copied to *directory*.  If not,
+    the test will be skipped.  Errors during copy are not caught.
+    """
+    shutil.copy(_get_xxmodule_path(), os.path.join(directory, 'xxmodule.c'))
+
+
+def _get_xxmodule_path():
+    source_name = 'xxmodule.c' if sys.version_info > (3, 9) else 'xxmodule-3.8.c'
+    return os.path.join(os.path.dirname(__file__), source_name)
+
+
+def fixup_build_ext(cmd):
+    """Function needed to make build_ext tests pass.
+
+    When Python was built with --enable-shared on Unix, -L. is not enough to
+    find libpython<blah>.so, because regrtest runs in a tempdir, not in the
+    source directory where the .so lives.
+
+    When Python was built with in debug mode on Windows, build_ext commands
+    need their debug attribute set, and it is not done automatically for
+    some reason.
+
+    This function handles both of these things.  Example use:
+
+        cmd = build_ext(dist)
+        support.fixup_build_ext(cmd)
+        cmd.ensure_finalized()
+
+    Unlike most other Unix platforms, Mac OS X embeds absolute paths
+    to shared libraries into executables, so the fixup is not needed there.
+    """
+    if os.name == 'nt':
+        cmd.debug = sys.executable.endswith('_d.exe')
+    elif sysconfig.get_config_var('Py_ENABLE_SHARED'):
+        # To further add to the shared builds fun on Unix, we can't just add
+        # library_dirs to the Extension() instance because that doesn't get
+        # plumbed through to the final compiler command.
+        runshared = sysconfig.get_config_var('RUNSHARED')
+        if runshared is None:
+            cmd.library_dirs = ['.']
+        else:
+            if sys.platform == 'darwin':
+                cmd.library_dirs = []
+            else:
+                name, equals, value = runshared.partition('=')
+                cmd.library_dirs = [d for d in value.split(os.pathsep) if d]
+
+
+def combine_markers(cls):
+    """
+    pytest will honor markers as found on the class, but when
+    markers are on multiple subclasses, only one appears. Use
+    this decorator to combine those markers.
+    """
+    cls.pytestmark = [
+        mark
+        for base in itertools.chain([cls], cls.__bases__)
+        for mark in getattr(base, 'pytestmark', [])
+    ]
+    return cls
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_archive_util.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_archive_util.py
new file mode 100644
index 00000000..3e4ed75a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_archive_util.py
@@ -0,0 +1,353 @@
+"""Tests for distutils.archive_util."""
+
+import functools
+import operator
+import os
+import pathlib
+import sys
+import tarfile
+from distutils import archive_util
+from distutils.archive_util import (
+    ARCHIVE_FORMATS,
+    check_archive_formats,
+    make_archive,
+    make_tarball,
+    make_zipfile,
+)
+from distutils.spawn import spawn
+from distutils.tests import support
+from os.path import splitdrive
+
+import path
+import pytest
+from test.support import patch
+
+from .unix_compat import UID_0_SUPPORT, grp, pwd, require_uid_0, require_unix_id
+
+
+def can_fs_encode(filename):
+    """
+    Return True if the filename can be saved in the file system.
+    """
+    if os.path.supports_unicode_filenames:
+        return True
+    try:
+        filename.encode(sys.getfilesystemencoding())
+    except UnicodeEncodeError:
+        return False
+    return True
+
+
+def all_equal(values):
+    return functools.reduce(operator.eq, values)
+
+
+def same_drive(*paths):
+    return all_equal(pathlib.Path(path).drive for path in paths)
+
+
+class ArchiveUtilTestCase(support.TempdirManager):
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_make_tarball(self, name='archive'):
+        # creating something to tar
+        tmpdir = self._create_files()
+        self._make_tarball(tmpdir, name, '.tar.gz')
+        # trying an uncompressed one
+        self._make_tarball(tmpdir, name, '.tar', compress=None)
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_make_tarball_gzip(self):
+        tmpdir = self._create_files()
+        self._make_tarball(tmpdir, 'archive', '.tar.gz', compress='gzip')
+
+    def test_make_tarball_bzip2(self):
+        pytest.importorskip('bz2')
+        tmpdir = self._create_files()
+        self._make_tarball(tmpdir, 'archive', '.tar.bz2', compress='bzip2')
+
+    def test_make_tarball_xz(self):
+        pytest.importorskip('lzma')
+        tmpdir = self._create_files()
+        self._make_tarball(tmpdir, 'archive', '.tar.xz', compress='xz')
+
+    @pytest.mark.skipif("not can_fs_encode('årchiv')")
+    def test_make_tarball_latin1(self):
+        """
+        Mirror test_make_tarball, except filename contains latin characters.
+        """
+        self.test_make_tarball('årchiv')  # note this isn't a real word
+
+    @pytest.mark.skipif("not can_fs_encode('のアーカイブ')")
+    def test_make_tarball_extended(self):
+        """
+        Mirror test_make_tarball, except filename contains extended
+        characters outside the latin charset.
+        """
+        self.test_make_tarball('のアーカイブ')  # japanese for archive
+
+    def _make_tarball(self, tmpdir, target_name, suffix, **kwargs):
+        tmpdir2 = self.mkdtemp()
+        if same_drive(tmpdir, tmpdir2):
+            pytest.skip("source and target should be on same drive")
+
+        base_name = os.path.join(tmpdir2, target_name)
+
+        # working with relative paths to avoid tar warnings
+        with path.Path(tmpdir):
+            make_tarball(splitdrive(base_name)[1], 'dist', **kwargs)
+
+        # check if the compressed tarball was created
+        tarball = base_name + suffix
+        assert os.path.exists(tarball)
+        assert self._tarinfo(tarball) == self._created_files
+
+    def _tarinfo(self, path):
+        tar = tarfile.open(path)
+        try:
+            names = tar.getnames()
+            names.sort()
+            return names
+        finally:
+            tar.close()
+
+    _zip_created_files = [
+        'dist/',
+        'dist/file1',
+        'dist/file2',
+        'dist/sub/',
+        'dist/sub/file3',
+        'dist/sub2/',
+    ]
+    _created_files = [p.rstrip('/') for p in _zip_created_files]
+
+    def _create_files(self):
+        # creating something to tar
+        tmpdir = self.mkdtemp()
+        dist = os.path.join(tmpdir, 'dist')
+        os.mkdir(dist)
+        self.write_file([dist, 'file1'], 'xxx')
+        self.write_file([dist, 'file2'], 'xxx')
+        os.mkdir(os.path.join(dist, 'sub'))
+        self.write_file([dist, 'sub', 'file3'], 'xxx')
+        os.mkdir(os.path.join(dist, 'sub2'))
+        return tmpdir
+
+    @pytest.mark.usefixtures('needs_zlib')
+    @pytest.mark.skipif("not (shutil.which('tar') and shutil.which('gzip'))")
+    def test_tarfile_vs_tar(self):
+        tmpdir = self._create_files()
+        tmpdir2 = self.mkdtemp()
+        base_name = os.path.join(tmpdir2, 'archive')
+        old_dir = os.getcwd()
+        os.chdir(tmpdir)
+        try:
+            make_tarball(base_name, 'dist')
+        finally:
+            os.chdir(old_dir)
+
+        # check if the compressed tarball was created
+        tarball = base_name + '.tar.gz'
+        assert os.path.exists(tarball)
+
+        # now create another tarball using `tar`
+        tarball2 = os.path.join(tmpdir, 'archive2.tar.gz')
+        tar_cmd = ['tar', '-cf', 'archive2.tar', 'dist']
+        gzip_cmd = ['gzip', '-f', '-9', 'archive2.tar']
+        old_dir = os.getcwd()
+        os.chdir(tmpdir)
+        try:
+            spawn(tar_cmd)
+            spawn(gzip_cmd)
+        finally:
+            os.chdir(old_dir)
+
+        assert os.path.exists(tarball2)
+        # let's compare both tarballs
+        assert self._tarinfo(tarball) == self._created_files
+        assert self._tarinfo(tarball2) == self._created_files
+
+        # trying an uncompressed one
+        base_name = os.path.join(tmpdir2, 'archive')
+        old_dir = os.getcwd()
+        os.chdir(tmpdir)
+        try:
+            make_tarball(base_name, 'dist', compress=None)
+        finally:
+            os.chdir(old_dir)
+        tarball = base_name + '.tar'
+        assert os.path.exists(tarball)
+
+        # now for a dry_run
+        base_name = os.path.join(tmpdir2, 'archive')
+        old_dir = os.getcwd()
+        os.chdir(tmpdir)
+        try:
+            make_tarball(base_name, 'dist', compress=None, dry_run=True)
+        finally:
+            os.chdir(old_dir)
+        tarball = base_name + '.tar'
+        assert os.path.exists(tarball)
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_make_zipfile(self):
+        zipfile = pytest.importorskip('zipfile')
+        # creating something to tar
+        tmpdir = self._create_files()
+        base_name = os.path.join(self.mkdtemp(), 'archive')
+        with path.Path(tmpdir):
+            make_zipfile(base_name, 'dist')
+
+        # check if the compressed tarball was created
+        tarball = base_name + '.zip'
+        assert os.path.exists(tarball)
+        with zipfile.ZipFile(tarball) as zf:
+            assert sorted(zf.namelist()) == self._zip_created_files
+
+    def test_make_zipfile_no_zlib(self):
+        zipfile = pytest.importorskip('zipfile')
+        patch(self, archive_util.zipfile, 'zlib', None)  # force zlib ImportError
+
+        called = []
+        zipfile_class = zipfile.ZipFile
+
+        def fake_zipfile(*a, **kw):
+            if kw.get('compression', None) == zipfile.ZIP_STORED:
+                called.append((a, kw))
+            return zipfile_class(*a, **kw)
+
+        patch(self, archive_util.zipfile, 'ZipFile', fake_zipfile)
+
+        # create something to tar and compress
+        tmpdir = self._create_files()
+        base_name = os.path.join(self.mkdtemp(), 'archive')
+        with path.Path(tmpdir):
+            make_zipfile(base_name, 'dist')
+
+        tarball = base_name + '.zip'
+        assert called == [((tarball, "w"), {'compression': zipfile.ZIP_STORED})]
+        assert os.path.exists(tarball)
+        with zipfile.ZipFile(tarball) as zf:
+            assert sorted(zf.namelist()) == self._zip_created_files
+
+    def test_check_archive_formats(self):
+        assert check_archive_formats(['gztar', 'xxx', 'zip']) == 'xxx'
+        assert (
+            check_archive_formats(['gztar', 'bztar', 'xztar', 'ztar', 'tar', 'zip'])
+            is None
+        )
+
+    def test_make_archive(self):
+        tmpdir = self.mkdtemp()
+        base_name = os.path.join(tmpdir, 'archive')
+        with pytest.raises(ValueError):
+            make_archive(base_name, 'xxx')
+
+    def test_make_archive_cwd(self):
+        current_dir = os.getcwd()
+
+        def _breaks(*args, **kw):
+            raise RuntimeError()
+
+        ARCHIVE_FORMATS['xxx'] = (_breaks, [], 'xxx file')
+        try:
+            try:
+                make_archive('xxx', 'xxx', root_dir=self.mkdtemp())
+            except Exception:
+                pass
+            assert os.getcwd() == current_dir
+        finally:
+            ARCHIVE_FORMATS.pop('xxx')
+
+    def test_make_archive_tar(self):
+        base_dir = self._create_files()
+        base_name = os.path.join(self.mkdtemp(), 'archive')
+        res = make_archive(base_name, 'tar', base_dir, 'dist')
+        assert os.path.exists(res)
+        assert os.path.basename(res) == 'archive.tar'
+        assert self._tarinfo(res) == self._created_files
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_make_archive_gztar(self):
+        base_dir = self._create_files()
+        base_name = os.path.join(self.mkdtemp(), 'archive')
+        res = make_archive(base_name, 'gztar', base_dir, 'dist')
+        assert os.path.exists(res)
+        assert os.path.basename(res) == 'archive.tar.gz'
+        assert self._tarinfo(res) == self._created_files
+
+    def test_make_archive_bztar(self):
+        pytest.importorskip('bz2')
+        base_dir = self._create_files()
+        base_name = os.path.join(self.mkdtemp(), 'archive')
+        res = make_archive(base_name, 'bztar', base_dir, 'dist')
+        assert os.path.exists(res)
+        assert os.path.basename(res) == 'archive.tar.bz2'
+        assert self._tarinfo(res) == self._created_files
+
+    def test_make_archive_xztar(self):
+        pytest.importorskip('lzma')
+        base_dir = self._create_files()
+        base_name = os.path.join(self.mkdtemp(), 'archive')
+        res = make_archive(base_name, 'xztar', base_dir, 'dist')
+        assert os.path.exists(res)
+        assert os.path.basename(res) == 'archive.tar.xz'
+        assert self._tarinfo(res) == self._created_files
+
+    def test_make_archive_owner_group(self):
+        # testing make_archive with owner and group, with various combinations
+        # this works even if there's not gid/uid support
+        if UID_0_SUPPORT:
+            group = grp.getgrgid(0)[0]
+            owner = pwd.getpwuid(0)[0]
+        else:
+            group = owner = 'root'
+
+        base_dir = self._create_files()
+        root_dir = self.mkdtemp()
+        base_name = os.path.join(self.mkdtemp(), 'archive')
+        res = make_archive(
+            base_name, 'zip', root_dir, base_dir, owner=owner, group=group
+        )
+        assert os.path.exists(res)
+
+        res = make_archive(base_name, 'zip', root_dir, base_dir)
+        assert os.path.exists(res)
+
+        res = make_archive(
+            base_name, 'tar', root_dir, base_dir, owner=owner, group=group
+        )
+        assert os.path.exists(res)
+
+        res = make_archive(
+            base_name, 'tar', root_dir, base_dir, owner='kjhkjhkjg', group='oihohoh'
+        )
+        assert os.path.exists(res)
+
+    @pytest.mark.usefixtures('needs_zlib')
+    @require_unix_id
+    @require_uid_0
+    def test_tarfile_root_owner(self):
+        tmpdir = self._create_files()
+        base_name = os.path.join(self.mkdtemp(), 'archive')
+        old_dir = os.getcwd()
+        os.chdir(tmpdir)
+        group = grp.getgrgid(0)[0]
+        owner = pwd.getpwuid(0)[0]
+        try:
+            archive_name = make_tarball(
+                base_name, 'dist', compress=None, owner=owner, group=group
+            )
+        finally:
+            os.chdir(old_dir)
+
+        # check if the compressed tarball was created
+        assert os.path.exists(archive_name)
+
+        # now checks the rights
+        archive = tarfile.open(archive_name)
+        try:
+            for member in archive.getmembers():
+                assert member.uid == 0
+                assert member.gid == 0
+        finally:
+            archive.close()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist.py
new file mode 100644
index 00000000..d5696fc3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist.py
@@ -0,0 +1,47 @@
+"""Tests for distutils.command.bdist."""
+
+from distutils.command.bdist import bdist
+from distutils.tests import support
+
+
+class TestBuild(support.TempdirManager):
+    def test_formats(self):
+        # let's create a command and make sure
+        # we can set the format
+        dist = self.create_dist()[1]
+        cmd = bdist(dist)
+        cmd.formats = ['gztar']
+        cmd.ensure_finalized()
+        assert cmd.formats == ['gztar']
+
+        # what formats does bdist offer?
+        formats = [
+            'bztar',
+            'gztar',
+            'rpm',
+            'tar',
+            'xztar',
+            'zip',
+            'ztar',
+        ]
+        found = sorted(cmd.format_commands)
+        assert found == formats
+
+    def test_skip_build(self):
+        # bug #10946: bdist --skip-build should trickle down to subcommands
+        dist = self.create_dist()[1]
+        cmd = bdist(dist)
+        cmd.skip_build = True
+        cmd.ensure_finalized()
+        dist.command_obj['bdist'] = cmd
+
+        names = [
+            'bdist_dumb',
+        ]  # bdist_rpm does not support --skip-build
+
+        for name in names:
+            subcmd = cmd.get_finalized_command(name)
+            if getattr(subcmd, '_unsupported', False):
+                # command is not supported on this build
+                continue
+            assert subcmd.skip_build, f'{name} should take --skip-build from bdist'
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist_dumb.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist_dumb.py
new file mode 100644
index 00000000..1fc51d24
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist_dumb.py
@@ -0,0 +1,78 @@
+"""Tests for distutils.command.bdist_dumb."""
+
+import os
+import sys
+import zipfile
+from distutils.command.bdist_dumb import bdist_dumb
+from distutils.core import Distribution
+from distutils.tests import support
+
+import pytest
+
+SETUP_PY = """\
+from distutils.core import setup
+import foo
+
+setup(name='foo', version='0.1', py_modules=['foo'],
+      url='xxx', author='xxx', author_email='xxx')
+
+"""
+
+
+@support.combine_markers
+@pytest.mark.usefixtures('save_env')
+@pytest.mark.usefixtures('save_argv')
+@pytest.mark.usefixtures('save_cwd')
+class TestBuildDumb(
+    support.TempdirManager,
+):
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_simple_built(self):
+        # let's create a simple package
+        tmp_dir = self.mkdtemp()
+        pkg_dir = os.path.join(tmp_dir, 'foo')
+        os.mkdir(pkg_dir)
+        self.write_file((pkg_dir, 'setup.py'), SETUP_PY)
+        self.write_file((pkg_dir, 'foo.py'), '#')
+        self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py')
+        self.write_file((pkg_dir, 'README'), '')
+
+        dist = Distribution({
+            'name': 'foo',
+            'version': '0.1',
+            'py_modules': ['foo'],
+            'url': 'xxx',
+            'author': 'xxx',
+            'author_email': 'xxx',
+        })
+        dist.script_name = 'setup.py'
+        os.chdir(pkg_dir)
+
+        sys.argv = ['setup.py']
+        cmd = bdist_dumb(dist)
+
+        # so the output is the same no matter
+        # what is the platform
+        cmd.format = 'zip'
+
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # see what we have
+        dist_created = os.listdir(os.path.join(pkg_dir, 'dist'))
+        base = f"{dist.get_fullname()}.{cmd.plat_name}.zip"
+
+        assert dist_created == [base]
+
+        # now let's check what we have in the zip file
+        fp = zipfile.ZipFile(os.path.join('dist', base))
+        try:
+            contents = fp.namelist()
+        finally:
+            fp.close()
+
+        contents = sorted(filter(None, map(os.path.basename, contents)))
+        wanted = ['foo-0.1-py{}.{}.egg-info'.format(*sys.version_info[:2]), 'foo.py']
+        if not sys.dont_write_bytecode:
+            wanted.append(f'foo.{sys.implementation.cache_tag}.pyc')
+        assert contents == sorted(wanted)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist_rpm.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist_rpm.py
new file mode 100644
index 00000000..75051430
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_bdist_rpm.py
@@ -0,0 +1,127 @@
+"""Tests for distutils.command.bdist_rpm."""
+
+import os
+import shutil  # noqa: F401
+import sys
+from distutils.command.bdist_rpm import bdist_rpm
+from distutils.core import Distribution
+from distutils.tests import support
+
+import pytest
+from test.support import requires_zlib
+
+SETUP_PY = """\
+from distutils.core import setup
+import foo
+
+setup(name='foo', version='0.1', py_modules=['foo'],
+      url='xxx', author='xxx', author_email='xxx')
+
+"""
+
+
+@pytest.fixture(autouse=True)
+def sys_executable_encodable():
+    try:
+        sys.executable.encode('UTF-8')
+    except UnicodeEncodeError:
+        pytest.skip("sys.executable is not encodable to UTF-8")
+
+
+mac_woes = pytest.mark.skipif(
+    "not sys.platform.startswith('linux')",
+    reason='spurious sdtout/stderr output under macOS',
+)
+
+
+@pytest.mark.usefixtures('save_env')
+@pytest.mark.usefixtures('save_argv')
+@pytest.mark.usefixtures('save_cwd')
+class TestBuildRpm(
+    support.TempdirManager,
+):
+    @mac_woes
+    @requires_zlib()
+    @pytest.mark.skipif("not shutil.which('rpm')")
+    @pytest.mark.skipif("not shutil.which('rpmbuild')")
+    def test_quiet(self):
+        # let's create a package
+        tmp_dir = self.mkdtemp()
+        os.environ['HOME'] = tmp_dir  # to confine dir '.rpmdb' creation
+        pkg_dir = os.path.join(tmp_dir, 'foo')
+        os.mkdir(pkg_dir)
+        self.write_file((pkg_dir, 'setup.py'), SETUP_PY)
+        self.write_file((pkg_dir, 'foo.py'), '#')
+        self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py')
+        self.write_file((pkg_dir, 'README'), '')
+
+        dist = Distribution({
+            'name': 'foo',
+            'version': '0.1',
+            'py_modules': ['foo'],
+            'url': 'xxx',
+            'author': 'xxx',
+            'author_email': 'xxx',
+        })
+        dist.script_name = 'setup.py'
+        os.chdir(pkg_dir)
+
+        sys.argv = ['setup.py']
+        cmd = bdist_rpm(dist)
+        cmd.fix_python = True
+
+        # running in quiet mode
+        cmd.quiet = True
+        cmd.ensure_finalized()
+        cmd.run()
+
+        dist_created = os.listdir(os.path.join(pkg_dir, 'dist'))
+        assert 'foo-0.1-1.noarch.rpm' in dist_created
+
+        # bug #2945: upload ignores bdist_rpm files
+        assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.src.rpm') in dist.dist_files
+        assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.noarch.rpm') in dist.dist_files
+
+    @mac_woes
+    @requires_zlib()
+    # https://bugs.python.org/issue1533164
+    @pytest.mark.skipif("not shutil.which('rpm')")
+    @pytest.mark.skipif("not shutil.which('rpmbuild')")
+    def test_no_optimize_flag(self):
+        # let's create a package that breaks bdist_rpm
+        tmp_dir = self.mkdtemp()
+        os.environ['HOME'] = tmp_dir  # to confine dir '.rpmdb' creation
+        pkg_dir = os.path.join(tmp_dir, 'foo')
+        os.mkdir(pkg_dir)
+        self.write_file((pkg_dir, 'setup.py'), SETUP_PY)
+        self.write_file((pkg_dir, 'foo.py'), '#')
+        self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py')
+        self.write_file((pkg_dir, 'README'), '')
+
+        dist = Distribution({
+            'name': 'foo',
+            'version': '0.1',
+            'py_modules': ['foo'],
+            'url': 'xxx',
+            'author': 'xxx',
+            'author_email': 'xxx',
+        })
+        dist.script_name = 'setup.py'
+        os.chdir(pkg_dir)
+
+        sys.argv = ['setup.py']
+        cmd = bdist_rpm(dist)
+        cmd.fix_python = True
+
+        cmd.quiet = True
+        cmd.ensure_finalized()
+        cmd.run()
+
+        dist_created = os.listdir(os.path.join(pkg_dir, 'dist'))
+        assert 'foo-0.1-1.noarch.rpm' in dist_created
+
+        # bug #2945: upload ignores bdist_rpm files
+        assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.src.rpm') in dist.dist_files
+        assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.noarch.rpm') in dist.dist_files
+
+        os.remove(os.path.join(pkg_dir, 'dist', 'foo-0.1-1.noarch.rpm'))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build.py
new file mode 100644
index 00000000..f7fe69ac
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build.py
@@ -0,0 +1,49 @@
+"""Tests for distutils.command.build."""
+
+import os
+import sys
+from distutils.command.build import build
+from distutils.tests import support
+from sysconfig import get_config_var, get_platform
+
+
+class TestBuild(support.TempdirManager):
+    def test_finalize_options(self):
+        pkg_dir, dist = self.create_dist()
+        cmd = build(dist)
+        cmd.finalize_options()
+
+        # if not specified, plat_name gets the current platform
+        assert cmd.plat_name == get_platform()
+
+        # build_purelib is build + lib
+        wanted = os.path.join(cmd.build_base, 'lib')
+        assert cmd.build_purelib == wanted
+
+        # build_platlib is 'build/lib.platform-cache_tag[-pydebug]'
+        # examples:
+        #   build/lib.macosx-10.3-i386-cpython39
+        plat_spec = f'.{cmd.plat_name}-{sys.implementation.cache_tag}'
+        if get_config_var('Py_GIL_DISABLED'):
+            plat_spec += 't'
+        if hasattr(sys, 'gettotalrefcount'):
+            assert cmd.build_platlib.endswith('-pydebug')
+            plat_spec += '-pydebug'
+        wanted = os.path.join(cmd.build_base, 'lib' + plat_spec)
+        assert cmd.build_platlib == wanted
+
+        # by default, build_lib = build_purelib
+        assert cmd.build_lib == cmd.build_purelib
+
+        # build_temp is build/temp.<plat>
+        wanted = os.path.join(cmd.build_base, 'temp' + plat_spec)
+        assert cmd.build_temp == wanted
+
+        # build_scripts is build/scripts-x.x
+        wanted = os.path.join(
+            cmd.build_base, f'scripts-{sys.version_info.major}.{sys.version_info.minor}'
+        )
+        assert cmd.build_scripts == wanted
+
+        # executable is os.path.normpath(sys.executable)
+        assert cmd.executable == os.path.normpath(sys.executable)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_clib.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_clib.py
new file mode 100644
index 00000000..f76f26bc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_clib.py
@@ -0,0 +1,134 @@
+"""Tests for distutils.command.build_clib."""
+
+import os
+from distutils.command.build_clib import build_clib
+from distutils.errors import DistutilsSetupError
+from distutils.tests import missing_compiler_executable, support
+
+import pytest
+
+
+class TestBuildCLib(support.TempdirManager):
+    def test_check_library_dist(self):
+        pkg_dir, dist = self.create_dist()
+        cmd = build_clib(dist)
+
+        # 'libraries' option must be a list
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_library_list('foo')
+
+        # each element of 'libraries' must a 2-tuple
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_library_list(['foo1', 'foo2'])
+
+        # first element of each tuple in 'libraries'
+        # must be a string (the library name)
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_library_list([(1, 'foo1'), ('name', 'foo2')])
+
+        # library name may not contain directory separators
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_library_list(
+                [('name', 'foo1'), ('another/name', 'foo2')],
+            )
+
+        # second element of each tuple must be a dictionary (build info)
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_library_list(
+                [('name', {}), ('another', 'foo2')],
+            )
+
+        # those work
+        libs = [('name', {}), ('name', {'ok': 'good'})]
+        cmd.check_library_list(libs)
+
+    def test_get_source_files(self):
+        pkg_dir, dist = self.create_dist()
+        cmd = build_clib(dist)
+
+        # "in 'libraries' option 'sources' must be present and must be
+        # a list of source filenames
+        cmd.libraries = [('name', {})]
+        with pytest.raises(DistutilsSetupError):
+            cmd.get_source_files()
+
+        cmd.libraries = [('name', {'sources': 1})]
+        with pytest.raises(DistutilsSetupError):
+            cmd.get_source_files()
+
+        cmd.libraries = [('name', {'sources': ['a', 'b']})]
+        assert cmd.get_source_files() == ['a', 'b']
+
+        cmd.libraries = [('name', {'sources': ('a', 'b')})]
+        assert cmd.get_source_files() == ['a', 'b']
+
+        cmd.libraries = [
+            ('name', {'sources': ('a', 'b')}),
+            ('name2', {'sources': ['c', 'd']}),
+        ]
+        assert cmd.get_source_files() == ['a', 'b', 'c', 'd']
+
+    def test_build_libraries(self):
+        pkg_dir, dist = self.create_dist()
+        cmd = build_clib(dist)
+
+        class FakeCompiler:
+            def compile(*args, **kw):
+                pass
+
+            create_static_lib = compile
+
+        cmd.compiler = FakeCompiler()
+
+        # build_libraries is also doing a bit of typo checking
+        lib = [('name', {'sources': 'notvalid'})]
+        with pytest.raises(DistutilsSetupError):
+            cmd.build_libraries(lib)
+
+        lib = [('name', {'sources': list()})]
+        cmd.build_libraries(lib)
+
+        lib = [('name', {'sources': tuple()})]
+        cmd.build_libraries(lib)
+
+    def test_finalize_options(self):
+        pkg_dir, dist = self.create_dist()
+        cmd = build_clib(dist)
+
+        cmd.include_dirs = 'one-dir'
+        cmd.finalize_options()
+        assert cmd.include_dirs == ['one-dir']
+
+        cmd.include_dirs = None
+        cmd.finalize_options()
+        assert cmd.include_dirs == []
+
+        cmd.distribution.libraries = 'WONTWORK'
+        with pytest.raises(DistutilsSetupError):
+            cmd.finalize_options()
+
+    @pytest.mark.skipif('platform.system() == "Windows"')
+    def test_run(self):
+        pkg_dir, dist = self.create_dist()
+        cmd = build_clib(dist)
+
+        foo_c = os.path.join(pkg_dir, 'foo.c')
+        self.write_file(foo_c, 'int main(void) { return 1;}\n')
+        cmd.libraries = [('foo', {'sources': [foo_c]})]
+
+        build_temp = os.path.join(pkg_dir, 'build')
+        os.mkdir(build_temp)
+        cmd.build_temp = build_temp
+        cmd.build_clib = build_temp
+
+        # Before we run the command, we want to make sure
+        # all commands are present on the system.
+        ccmd = missing_compiler_executable()
+        if ccmd is not None:
+            self.skipTest(f'The {ccmd!r} command is not found')
+
+        # this should work
+        cmd.run()
+
+        # let's check the result
+        assert 'libfoo.a' in os.listdir(build_temp)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_ext.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_ext.py
new file mode 100644
index 00000000..dab0507f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_ext.py
@@ -0,0 +1,628 @@
+import contextlib
+import glob
+import importlib
+import os.path
+import platform
+import re
+import shutil
+import site
+import subprocess
+import sys
+import tempfile
+import textwrap
+import time
+from distutils import sysconfig
+from distutils.command.build_ext import build_ext
+from distutils.core import Distribution
+from distutils.errors import (
+    CompileError,
+    DistutilsPlatformError,
+    DistutilsSetupError,
+    UnknownFileError,
+)
+from distutils.extension import Extension
+from distutils.tests import missing_compiler_executable
+from distutils.tests.support import TempdirManager, copy_xxmodule_c, fixup_build_ext
+from io import StringIO
+
+import jaraco.path
+import path
+import pytest
+from test import support
+
+from .compat import py39 as import_helper
+
+
+@pytest.fixture()
+def user_site_dir(request):
+    self = request.instance
+    self.tmp_dir = self.mkdtemp()
+    self.tmp_path = path.Path(self.tmp_dir)
+    from distutils.command import build_ext
+
+    orig_user_base = site.USER_BASE
+
+    site.USER_BASE = self.mkdtemp()
+    build_ext.USER_BASE = site.USER_BASE
+
+    # bpo-30132: On Windows, a .pdb file may be created in the current
+    # working directory. Create a temporary working directory to cleanup
+    # everything at the end of the test.
+    with self.tmp_path:
+        yield
+
+    site.USER_BASE = orig_user_base
+    build_ext.USER_BASE = orig_user_base
+
+    if sys.platform == 'cygwin':
+        time.sleep(1)
+
+
+@contextlib.contextmanager
+def safe_extension_import(name, path):
+    with import_helper.CleanImport(name):
+        with extension_redirect(name, path) as new_path:
+            with import_helper.DirsOnSysPath(new_path):
+                yield
+
+
+@contextlib.contextmanager
+def extension_redirect(mod, path):
+    """
+    Tests will fail to tear down an extension module if it's been imported.
+
+    Before importing, copy the file to a temporary directory that won't
+    be cleaned up. Yield the new path.
+    """
+    if platform.system() != "Windows" and sys.platform != "cygwin":
+        yield path
+        return
+    with import_helper.DirsOnSysPath(path):
+        spec = importlib.util.find_spec(mod)
+    filename = os.path.basename(spec.origin)
+    trash_dir = tempfile.mkdtemp(prefix='deleteme')
+    dest = os.path.join(trash_dir, os.path.basename(filename))
+    shutil.copy(spec.origin, dest)
+    yield trash_dir
+    # TODO: can the file be scheduled for deletion?
+
+
+@pytest.mark.usefixtures('user_site_dir')
+class TestBuildExt(TempdirManager):
+    def build_ext(self, *args, **kwargs):
+        return build_ext(*args, **kwargs)
+
+    @pytest.mark.parametrize("copy_so", [False])
+    def test_build_ext(self, copy_so):
+        missing_compiler_executable()
+        copy_xxmodule_c(self.tmp_dir)
+        xx_c = os.path.join(self.tmp_dir, 'xxmodule.c')
+        xx_ext = Extension('xx', [xx_c])
+        if sys.platform != "win32":
+            if not copy_so:
+                xx_ext = Extension(
+                    'xx',
+                    [xx_c],
+                    library_dirs=['/usr/lib'],
+                    libraries=['z'],
+                    runtime_library_dirs=['/usr/lib'],
+                )
+            elif sys.platform == 'linux':
+                libz_so = {
+                    os.path.realpath(name) for name in glob.iglob('/usr/lib*/libz.so*')
+                }
+                libz_so = sorted(libz_so, key=lambda lib_path: len(lib_path))
+                shutil.copyfile(libz_so[-1], '/tmp/libxx_z.so')
+
+                xx_ext = Extension(
+                    'xx',
+                    [xx_c],
+                    library_dirs=['/tmp'],
+                    libraries=['xx_z'],
+                    runtime_library_dirs=['/tmp'],
+                )
+        dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]})
+        dist.package_dir = self.tmp_dir
+        cmd = self.build_ext(dist)
+        fixup_build_ext(cmd)
+        cmd.build_lib = self.tmp_dir
+        cmd.build_temp = self.tmp_dir
+
+        old_stdout = sys.stdout
+        if not support.verbose:
+            # silence compiler output
+            sys.stdout = StringIO()
+        try:
+            cmd.ensure_finalized()
+            cmd.run()
+        finally:
+            sys.stdout = old_stdout
+
+        with safe_extension_import('xx', self.tmp_dir):
+            self._test_xx(copy_so)
+
+        if sys.platform == 'linux' and copy_so:
+            os.unlink('/tmp/libxx_z.so')
+
+    @staticmethod
+    def _test_xx(copy_so):
+        import xx  # type: ignore[import-not-found] # Module generated for tests
+
+        for attr in ('error', 'foo', 'new', 'roj'):
+            assert hasattr(xx, attr)
+
+        assert xx.foo(2, 5) == 7
+        assert xx.foo(13, 15) == 28
+        assert xx.new().demo() is None
+        if support.HAVE_DOCSTRINGS:
+            doc = 'This is a template module just for instruction.'
+            assert xx.__doc__ == doc
+        assert isinstance(xx.Null(), xx.Null)
+        assert isinstance(xx.Str(), xx.Str)
+
+        if sys.platform == 'linux':
+            so_headers = subprocess.check_output(
+                ["readelf", "-d", xx.__file__], universal_newlines=True
+            )
+            import pprint
+
+            pprint.pprint(so_headers)
+            rpaths = [
+                rpath
+                for line in so_headers.split("\n")
+                if "RPATH" in line or "RUNPATH" in line
+                for rpath in line.split()[2][1:-1].split(":")
+            ]
+            if not copy_so:
+                pprint.pprint(rpaths)
+                # Linked against a library in /usr/lib{,64}
+                assert "/usr/lib" not in rpaths and "/usr/lib64" not in rpaths
+            else:
+                # Linked against a library in /tmp
+                assert "/tmp" in rpaths
+                # The import is the real test here
+
+    def test_solaris_enable_shared(self):
+        dist = Distribution({'name': 'xx'})
+        cmd = self.build_ext(dist)
+        old = sys.platform
+
+        sys.platform = 'sunos'  # fooling finalize_options
+        from distutils.sysconfig import _config_vars
+
+        old_var = _config_vars.get('Py_ENABLE_SHARED')
+        _config_vars['Py_ENABLE_SHARED'] = True
+        try:
+            cmd.ensure_finalized()
+        finally:
+            sys.platform = old
+            if old_var is None:
+                del _config_vars['Py_ENABLE_SHARED']
+            else:
+                _config_vars['Py_ENABLE_SHARED'] = old_var
+
+        # make sure we get some library dirs under solaris
+        assert len(cmd.library_dirs) > 0
+
+    def test_user_site(self):
+        import site
+
+        dist = Distribution({'name': 'xx'})
+        cmd = self.build_ext(dist)
+
+        # making sure the user option is there
+        options = [name for name, short, label in cmd.user_options]
+        assert 'user' in options
+
+        # setting a value
+        cmd.user = True
+
+        # setting user based lib and include
+        lib = os.path.join(site.USER_BASE, 'lib')
+        incl = os.path.join(site.USER_BASE, 'include')
+        os.mkdir(lib)
+        os.mkdir(incl)
+
+        # let's run finalize
+        cmd.ensure_finalized()
+
+        # see if include_dirs and library_dirs
+        # were set
+        assert lib in cmd.library_dirs
+        assert lib in cmd.rpath
+        assert incl in cmd.include_dirs
+
+    def test_optional_extension(self):
+        # this extension will fail, but let's ignore this failure
+        # with the optional argument.
+        modules = [Extension('foo', ['xxx'], optional=False)]
+        dist = Distribution({'name': 'xx', 'ext_modules': modules})
+        cmd = self.build_ext(dist)
+        cmd.ensure_finalized()
+        with pytest.raises((UnknownFileError, CompileError)):
+            cmd.run()  # should raise an error
+
+        modules = [Extension('foo', ['xxx'], optional=True)]
+        dist = Distribution({'name': 'xx', 'ext_modules': modules})
+        cmd = self.build_ext(dist)
+        cmd.ensure_finalized()
+        cmd.run()  # should pass
+
+    def test_finalize_options(self):
+        # Make sure Python's include directories (for Python.h, pyconfig.h,
+        # etc.) are in the include search path.
+        modules = [Extension('foo', ['xxx'], optional=False)]
+        dist = Distribution({'name': 'xx', 'ext_modules': modules})
+        cmd = self.build_ext(dist)
+        cmd.finalize_options()
+
+        py_include = sysconfig.get_python_inc()
+        for p in py_include.split(os.path.pathsep):
+            assert p in cmd.include_dirs
+
+        plat_py_include = sysconfig.get_python_inc(plat_specific=True)
+        for p in plat_py_include.split(os.path.pathsep):
+            assert p in cmd.include_dirs
+
+        # make sure cmd.libraries is turned into a list
+        # if it's a string
+        cmd = self.build_ext(dist)
+        cmd.libraries = 'my_lib, other_lib lastlib'
+        cmd.finalize_options()
+        assert cmd.libraries == ['my_lib', 'other_lib', 'lastlib']
+
+        # make sure cmd.library_dirs is turned into a list
+        # if it's a string
+        cmd = self.build_ext(dist)
+        cmd.library_dirs = f'my_lib_dir{os.pathsep}other_lib_dir'
+        cmd.finalize_options()
+        assert 'my_lib_dir' in cmd.library_dirs
+        assert 'other_lib_dir' in cmd.library_dirs
+
+        # make sure rpath is turned into a list
+        # if it's a string
+        cmd = self.build_ext(dist)
+        cmd.rpath = f'one{os.pathsep}two'
+        cmd.finalize_options()
+        assert cmd.rpath == ['one', 'two']
+
+        # make sure cmd.link_objects is turned into a list
+        # if it's a string
+        cmd = build_ext(dist)
+        cmd.link_objects = 'one two,three'
+        cmd.finalize_options()
+        assert cmd.link_objects == ['one', 'two', 'three']
+
+        # XXX more tests to perform for win32
+
+        # make sure define is turned into 2-tuples
+        # strings if they are ','-separated strings
+        cmd = self.build_ext(dist)
+        cmd.define = 'one,two'
+        cmd.finalize_options()
+        assert cmd.define == [('one', '1'), ('two', '1')]
+
+        # make sure undef is turned into a list of
+        # strings if they are ','-separated strings
+        cmd = self.build_ext(dist)
+        cmd.undef = 'one,two'
+        cmd.finalize_options()
+        assert cmd.undef == ['one', 'two']
+
+        # make sure swig_opts is turned into a list
+        cmd = self.build_ext(dist)
+        cmd.swig_opts = None
+        cmd.finalize_options()
+        assert cmd.swig_opts == []
+
+        cmd = self.build_ext(dist)
+        cmd.swig_opts = '1 2'
+        cmd.finalize_options()
+        assert cmd.swig_opts == ['1', '2']
+
+    def test_check_extensions_list(self):
+        dist = Distribution()
+        cmd = self.build_ext(dist)
+        cmd.finalize_options()
+
+        # 'extensions' option must be a list of Extension instances
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_extensions_list('foo')
+
+        # each element of 'ext_modules' option must be an
+        # Extension instance or 2-tuple
+        exts = [('bar', 'foo', 'bar'), 'foo']
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_extensions_list(exts)
+
+        # first element of each tuple in 'ext_modules'
+        # must be the extension name (a string) and match
+        # a python dotted-separated name
+        exts = [('foo-bar', '')]
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_extensions_list(exts)
+
+        # second element of each tuple in 'ext_modules'
+        # must be a dictionary (build info)
+        exts = [('foo.bar', '')]
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_extensions_list(exts)
+
+        # ok this one should pass
+        exts = [('foo.bar', {'sources': [''], 'libraries': 'foo', 'some': 'bar'})]
+        cmd.check_extensions_list(exts)
+        ext = exts[0]
+        assert isinstance(ext, Extension)
+
+        # check_extensions_list adds in ext the values passed
+        # when they are in ('include_dirs', 'library_dirs', 'libraries'
+        # 'extra_objects', 'extra_compile_args', 'extra_link_args')
+        assert ext.libraries == 'foo'
+        assert not hasattr(ext, 'some')
+
+        # 'macros' element of build info dict must be 1- or 2-tuple
+        exts = [
+            (
+                'foo.bar',
+                {
+                    'sources': [''],
+                    'libraries': 'foo',
+                    'some': 'bar',
+                    'macros': [('1', '2', '3'), 'foo'],
+                },
+            )
+        ]
+        with pytest.raises(DistutilsSetupError):
+            cmd.check_extensions_list(exts)
+
+        exts[0][1]['macros'] = [('1', '2'), ('3',)]
+        cmd.check_extensions_list(exts)
+        assert exts[0].undef_macros == ['3']
+        assert exts[0].define_macros == [('1', '2')]
+
+    def test_get_source_files(self):
+        modules = [Extension('foo', ['xxx'], optional=False)]
+        dist = Distribution({'name': 'xx', 'ext_modules': modules})
+        cmd = self.build_ext(dist)
+        cmd.ensure_finalized()
+        assert cmd.get_source_files() == ['xxx']
+
+    def test_unicode_module_names(self):
+        modules = [
+            Extension('foo', ['aaa'], optional=False),
+            Extension('föö', ['uuu'], optional=False),
+        ]
+        dist = Distribution({'name': 'xx', 'ext_modules': modules})
+        cmd = self.build_ext(dist)
+        cmd.ensure_finalized()
+        assert re.search(r'foo(_d)?\..*', cmd.get_ext_filename(modules[0].name))
+        assert re.search(r'föö(_d)?\..*', cmd.get_ext_filename(modules[1].name))
+        assert cmd.get_export_symbols(modules[0]) == ['PyInit_foo']
+        assert cmd.get_export_symbols(modules[1]) == ['PyInitU_f_1gaa']
+
+    def test_export_symbols__init__(self):
+        # https://github.com/python/cpython/issues/80074
+        # https://github.com/pypa/setuptools/issues/4826
+        modules = [
+            Extension('foo.__init__', ['aaa']),
+            Extension('föö.__init__', ['uuu']),
+        ]
+        dist = Distribution({'name': 'xx', 'ext_modules': modules})
+        cmd = self.build_ext(dist)
+        cmd.ensure_finalized()
+        assert cmd.get_export_symbols(modules[0]) == ['PyInit_foo']
+        assert cmd.get_export_symbols(modules[1]) == ['PyInitU_f_1gaa']
+
+    def test_compiler_option(self):
+        # cmd.compiler is an option and
+        # should not be overridden by a compiler instance
+        # when the command is run
+        dist = Distribution()
+        cmd = self.build_ext(dist)
+        cmd.compiler = 'unix'
+        cmd.ensure_finalized()
+        cmd.run()
+        assert cmd.compiler == 'unix'
+
+    def test_get_outputs(self):
+        missing_compiler_executable()
+        tmp_dir = self.mkdtemp()
+        c_file = os.path.join(tmp_dir, 'foo.c')
+        self.write_file(c_file, 'void PyInit_foo(void) {}\n')
+        ext = Extension('foo', [c_file], optional=False)
+        dist = Distribution({'name': 'xx', 'ext_modules': [ext]})
+        cmd = self.build_ext(dist)
+        fixup_build_ext(cmd)
+        cmd.ensure_finalized()
+        assert len(cmd.get_outputs()) == 1
+
+        cmd.build_lib = os.path.join(self.tmp_dir, 'build')
+        cmd.build_temp = os.path.join(self.tmp_dir, 'tempt')
+
+        # issue #5977 : distutils build_ext.get_outputs
+        # returns wrong result with --inplace
+        other_tmp_dir = os.path.realpath(self.mkdtemp())
+        old_wd = os.getcwd()
+        os.chdir(other_tmp_dir)
+        try:
+            cmd.inplace = True
+            cmd.run()
+            so_file = cmd.get_outputs()[0]
+        finally:
+            os.chdir(old_wd)
+        assert os.path.exists(so_file)
+        ext_suffix = sysconfig.get_config_var('EXT_SUFFIX')
+        assert so_file.endswith(ext_suffix)
+        so_dir = os.path.dirname(so_file)
+        assert so_dir == other_tmp_dir
+
+        cmd.inplace = False
+        cmd.compiler = None
+        cmd.run()
+        so_file = cmd.get_outputs()[0]
+        assert os.path.exists(so_file)
+        assert so_file.endswith(ext_suffix)
+        so_dir = os.path.dirname(so_file)
+        assert so_dir == cmd.build_lib
+
+        # inplace = False, cmd.package = 'bar'
+        build_py = cmd.get_finalized_command('build_py')
+        build_py.package_dir = {'': 'bar'}
+        path = cmd.get_ext_fullpath('foo')
+        # checking that the last directory is the build_dir
+        path = os.path.split(path)[0]
+        assert path == cmd.build_lib
+
+        # inplace = True, cmd.package = 'bar'
+        cmd.inplace = True
+        other_tmp_dir = os.path.realpath(self.mkdtemp())
+        old_wd = os.getcwd()
+        os.chdir(other_tmp_dir)
+        try:
+            path = cmd.get_ext_fullpath('foo')
+        finally:
+            os.chdir(old_wd)
+        # checking that the last directory is bar
+        path = os.path.split(path)[0]
+        lastdir = os.path.split(path)[-1]
+        assert lastdir == 'bar'
+
+    def test_ext_fullpath(self):
+        ext = sysconfig.get_config_var('EXT_SUFFIX')
+        # building lxml.etree inplace
+        # etree_c = os.path.join(self.tmp_dir, 'lxml.etree.c')
+        # etree_ext = Extension('lxml.etree', [etree_c])
+        # dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]})
+        dist = Distribution()
+        cmd = self.build_ext(dist)
+        cmd.inplace = True
+        cmd.distribution.package_dir = {'': 'src'}
+        cmd.distribution.packages = ['lxml', 'lxml.html']
+        curdir = os.getcwd()
+        wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext)
+        path = cmd.get_ext_fullpath('lxml.etree')
+        assert wanted == path
+
+        # building lxml.etree not inplace
+        cmd.inplace = False
+        cmd.build_lib = os.path.join(curdir, 'tmpdir')
+        wanted = os.path.join(curdir, 'tmpdir', 'lxml', 'etree' + ext)
+        path = cmd.get_ext_fullpath('lxml.etree')
+        assert wanted == path
+
+        # building twisted.runner.portmap not inplace
+        build_py = cmd.get_finalized_command('build_py')
+        build_py.package_dir = {}
+        cmd.distribution.packages = ['twisted', 'twisted.runner.portmap']
+        path = cmd.get_ext_fullpath('twisted.runner.portmap')
+        wanted = os.path.join(curdir, 'tmpdir', 'twisted', 'runner', 'portmap' + ext)
+        assert wanted == path
+
+        # building twisted.runner.portmap inplace
+        cmd.inplace = True
+        path = cmd.get_ext_fullpath('twisted.runner.portmap')
+        wanted = os.path.join(curdir, 'twisted', 'runner', 'portmap' + ext)
+        assert wanted == path
+
+    @pytest.mark.skipif('platform.system() != "Darwin"')
+    @pytest.mark.usefixtures('save_env')
+    def test_deployment_target_default(self):
+        # Issue 9516: Test that, in the absence of the environment variable,
+        # an extension module is compiled with the same deployment target as
+        #  the interpreter.
+        self._try_compile_deployment_target('==', None)
+
+    @pytest.mark.skipif('platform.system() != "Darwin"')
+    @pytest.mark.usefixtures('save_env')
+    def test_deployment_target_too_low(self):
+        # Issue 9516: Test that an extension module is not allowed to be
+        # compiled with a deployment target less than that of the interpreter.
+        with pytest.raises(DistutilsPlatformError):
+            self._try_compile_deployment_target('>', '10.1')
+
+    @pytest.mark.skipif('platform.system() != "Darwin"')
+    @pytest.mark.usefixtures('save_env')
+    def test_deployment_target_higher_ok(self):  # pragma: no cover
+        # Issue 9516: Test that an extension module can be compiled with a
+        # deployment target higher than that of the interpreter: the ext
+        # module may depend on some newer OS feature.
+        deptarget = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
+        if deptarget:
+            # increment the minor version number (i.e. 10.6 -> 10.7)
+            deptarget = [int(x) for x in deptarget.split('.')]
+            deptarget[-1] += 1
+            deptarget = '.'.join(str(i) for i in deptarget)
+            self._try_compile_deployment_target('<', deptarget)
+
+    def _try_compile_deployment_target(self, operator, target):  # pragma: no cover
+        if target is None:
+            if os.environ.get('MACOSX_DEPLOYMENT_TARGET'):
+                del os.environ['MACOSX_DEPLOYMENT_TARGET']
+        else:
+            os.environ['MACOSX_DEPLOYMENT_TARGET'] = target
+
+        jaraco.path.build(
+            {
+                'deptargetmodule.c': textwrap.dedent(f"""\
+                    #include <AvailabilityMacros.h>
+
+                    int dummy;
+
+                    #if TARGET {operator} MAC_OS_X_VERSION_MIN_REQUIRED
+                    #else
+                    #error "Unexpected target"
+                    #endif
+
+                    """),
+            },
+            self.tmp_path,
+        )
+
+        # get the deployment target that the interpreter was built with
+        target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
+        target = tuple(map(int, target.split('.')[0:2]))
+        # format the target value as defined in the Apple
+        # Availability Macros.  We can't use the macro names since
+        # at least one value we test with will not exist yet.
+        if target[:2] < (10, 10):
+            # for 10.1 through 10.9.x -> "10n0"
+            tmpl = '{:02}{:01}0'
+        else:
+            # for 10.10 and beyond -> "10nn00"
+            if len(target) >= 2:
+                tmpl = '{:02}{:02}00'
+            else:
+                # 11 and later can have no minor version (11 instead of 11.0)
+                tmpl = '{:02}0000'
+        target = tmpl.format(*target)
+        deptarget_ext = Extension(
+            'deptarget',
+            [self.tmp_path / 'deptargetmodule.c'],
+            extra_compile_args=[f'-DTARGET={target}'],
+        )
+        dist = Distribution({'name': 'deptarget', 'ext_modules': [deptarget_ext]})
+        dist.package_dir = self.tmp_dir
+        cmd = self.build_ext(dist)
+        cmd.build_lib = self.tmp_dir
+        cmd.build_temp = self.tmp_dir
+
+        try:
+            old_stdout = sys.stdout
+            if not support.verbose:
+                # silence compiler output
+                sys.stdout = StringIO()
+            try:
+                cmd.ensure_finalized()
+                cmd.run()
+            finally:
+                sys.stdout = old_stdout
+
+        except CompileError:
+            self.fail("Wrong deployment target during compilation")
+
+
+class TestParallelBuildExt(TestBuildExt):
+    def build_ext(self, *args, **kwargs):
+        build_ext = super().build_ext(*args, **kwargs)
+        build_ext.parallel = True
+        return build_ext
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_py.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_py.py
new file mode 100644
index 00000000..b316ed43
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_py.py
@@ -0,0 +1,196 @@
+"""Tests for distutils.command.build_py."""
+
+import os
+import sys
+from distutils.command.build_py import build_py
+from distutils.core import Distribution
+from distutils.errors import DistutilsFileError
+from distutils.tests import support
+
+import jaraco.path
+import pytest
+
+
+@support.combine_markers
+class TestBuildPy(support.TempdirManager):
+    def test_package_data(self):
+        sources = self.mkdtemp()
+        jaraco.path.build(
+            {
+                '__init__.py': "# Pretend this is a package.",
+                'README.txt': 'Info about this package',
+            },
+            sources,
+        )
+
+        destination = self.mkdtemp()
+
+        dist = Distribution({"packages": ["pkg"], "package_dir": {"pkg": sources}})
+        # script_name need not exist, it just need to be initialized
+        dist.script_name = os.path.join(sources, "setup.py")
+        dist.command_obj["build"] = support.DummyCommand(
+            force=False, build_lib=destination
+        )
+        dist.packages = ["pkg"]
+        dist.package_data = {"pkg": ["README.txt"]}
+        dist.package_dir = {"pkg": sources}
+
+        cmd = build_py(dist)
+        cmd.compile = True
+        cmd.ensure_finalized()
+        assert cmd.package_data == dist.package_data
+
+        cmd.run()
+
+        # This makes sure the list of outputs includes byte-compiled
+        # files for Python modules but not for package data files
+        # (there shouldn't *be* byte-code files for those!).
+        assert len(cmd.get_outputs()) == 3
+        pkgdest = os.path.join(destination, "pkg")
+        files = os.listdir(pkgdest)
+        pycache_dir = os.path.join(pkgdest, "__pycache__")
+        assert "__init__.py" in files
+        assert "README.txt" in files
+        if sys.dont_write_bytecode:
+            assert not os.path.exists(pycache_dir)
+        else:
+            pyc_files = os.listdir(pycache_dir)
+            assert f"__init__.{sys.implementation.cache_tag}.pyc" in pyc_files
+
+    def test_empty_package_dir(self):
+        # See bugs #1668596/#1720897
+        sources = self.mkdtemp()
+        jaraco.path.build({'__init__.py': '', 'doc': {'testfile': ''}}, sources)
+
+        os.chdir(sources)
+        dist = Distribution({
+            "packages": ["pkg"],
+            "package_dir": {"pkg": ""},
+            "package_data": {"pkg": ["doc/*"]},
+        })
+        # script_name need not exist, it just need to be initialized
+        dist.script_name = os.path.join(sources, "setup.py")
+        dist.script_args = ["build"]
+        dist.parse_command_line()
+
+        try:
+            dist.run_commands()
+        except DistutilsFileError:
+            self.fail("failed package_data test when package_dir is ''")
+
+    @pytest.mark.skipif('sys.dont_write_bytecode')
+    def test_byte_compile(self):
+        project_dir, dist = self.create_dist(py_modules=['boiledeggs'])
+        os.chdir(project_dir)
+        self.write_file('boiledeggs.py', 'import antigravity')
+        cmd = build_py(dist)
+        cmd.compile = True
+        cmd.build_lib = 'here'
+        cmd.finalize_options()
+        cmd.run()
+
+        found = os.listdir(cmd.build_lib)
+        assert sorted(found) == ['__pycache__', 'boiledeggs.py']
+        found = os.listdir(os.path.join(cmd.build_lib, '__pycache__'))
+        assert found == [f'boiledeggs.{sys.implementation.cache_tag}.pyc']
+
+    @pytest.mark.skipif('sys.dont_write_bytecode')
+    def test_byte_compile_optimized(self):
+        project_dir, dist = self.create_dist(py_modules=['boiledeggs'])
+        os.chdir(project_dir)
+        self.write_file('boiledeggs.py', 'import antigravity')
+        cmd = build_py(dist)
+        cmd.compile = False
+        cmd.optimize = 1
+        cmd.build_lib = 'here'
+        cmd.finalize_options()
+        cmd.run()
+
+        found = os.listdir(cmd.build_lib)
+        assert sorted(found) == ['__pycache__', 'boiledeggs.py']
+        found = os.listdir(os.path.join(cmd.build_lib, '__pycache__'))
+        expect = f'boiledeggs.{sys.implementation.cache_tag}.opt-1.pyc'
+        assert sorted(found) == [expect]
+
+    def test_dir_in_package_data(self):
+        """
+        A directory in package_data should not be added to the filelist.
+        """
+        # See bug 19286
+        sources = self.mkdtemp()
+        jaraco.path.build(
+            {
+                'pkg': {
+                    '__init__.py': '',
+                    'doc': {
+                        'testfile': '',
+                        # create a directory that could be incorrectly detected as a file
+                        'otherdir': {},
+                    },
+                }
+            },
+            sources,
+        )
+
+        os.chdir(sources)
+        dist = Distribution({"packages": ["pkg"], "package_data": {"pkg": ["doc/*"]}})
+        # script_name need not exist, it just need to be initialized
+        dist.script_name = os.path.join(sources, "setup.py")
+        dist.script_args = ["build"]
+        dist.parse_command_line()
+
+        try:
+            dist.run_commands()
+        except DistutilsFileError:
+            self.fail("failed package_data when data dir includes a dir")
+
+    def test_dont_write_bytecode(self, caplog):
+        # makes sure byte_compile is not used
+        dist = self.create_dist()[1]
+        cmd = build_py(dist)
+        cmd.compile = True
+        cmd.optimize = 1
+
+        old_dont_write_bytecode = sys.dont_write_bytecode
+        sys.dont_write_bytecode = True
+        try:
+            cmd.byte_compile([])
+        finally:
+            sys.dont_write_bytecode = old_dont_write_bytecode
+
+        assert 'byte-compiling is disabled' in caplog.records[0].message
+
+    def test_namespace_package_does_not_warn(self, caplog):
+        """
+        Originally distutils implementation did not account for PEP 420
+        and included warns for package directories that did not contain
+        ``__init__.py`` files.
+        After the acceptance of PEP 420, these warnings don't make more sense
+        so we want to ensure there are not displayed to not confuse the users.
+        """
+        # Create a fake project structure with a package namespace:
+        tmp = self.mkdtemp()
+        jaraco.path.build({'ns': {'pkg': {'module.py': ''}}}, tmp)
+        os.chdir(tmp)
+
+        # Configure the package:
+        attrs = {
+            "name": "ns.pkg",
+            "packages": ["ns", "ns.pkg"],
+            "script_name": "setup.py",
+        }
+        dist = Distribution(attrs)
+
+        # Run code paths that would trigger the trap:
+        cmd = dist.get_command_obj("build_py")
+        cmd.finalize_options()
+        modules = cmd.find_all_modules()
+        assert len(modules) == 1
+        module_path = modules[0][-1]
+        assert module_path.replace(os.sep, "/") == "ns/pkg/module.py"
+
+        cmd.run()
+
+        assert not any(
+            "package init file" in msg and "not found" in msg for msg in caplog.messages
+        )
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_scripts.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_scripts.py
new file mode 100644
index 00000000..3582f691
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_build_scripts.py
@@ -0,0 +1,96 @@
+"""Tests for distutils.command.build_scripts."""
+
+import os
+import textwrap
+from distutils import sysconfig
+from distutils.command.build_scripts import build_scripts
+from distutils.core import Distribution
+from distutils.tests import support
+
+import jaraco.path
+
+
+class TestBuildScripts(support.TempdirManager):
+    def test_default_settings(self):
+        cmd = self.get_build_scripts_cmd("/foo/bar", [])
+        assert not cmd.force
+        assert cmd.build_dir is None
+
+        cmd.finalize_options()
+
+        assert cmd.force
+        assert cmd.build_dir == "/foo/bar"
+
+    def test_build(self):
+        source = self.mkdtemp()
+        target = self.mkdtemp()
+        expected = self.write_sample_scripts(source)
+
+        cmd = self.get_build_scripts_cmd(
+            target, [os.path.join(source, fn) for fn in expected]
+        )
+        cmd.finalize_options()
+        cmd.run()
+
+        built = os.listdir(target)
+        for name in expected:
+            assert name in built
+
+    def get_build_scripts_cmd(self, target, scripts):
+        import sys
+
+        dist = Distribution()
+        dist.scripts = scripts
+        dist.command_obj["build"] = support.DummyCommand(
+            build_scripts=target, force=True, executable=sys.executable
+        )
+        return build_scripts(dist)
+
+    @staticmethod
+    def write_sample_scripts(dir):
+        spec = {
+            'script1.py': textwrap.dedent("""
+                #! /usr/bin/env python2.3
+                # bogus script w/ Python sh-bang
+                pass
+                """).lstrip(),
+            'script2.py': textwrap.dedent("""
+                #!/usr/bin/python
+                # bogus script w/ Python sh-bang
+                pass
+                """).lstrip(),
+            'shell.sh': textwrap.dedent("""
+                #!/bin/sh
+                # bogus shell script w/ sh-bang
+                exit 0
+                """).lstrip(),
+        }
+        jaraco.path.build(spec, dir)
+        return list(spec)
+
+    def test_version_int(self):
+        source = self.mkdtemp()
+        target = self.mkdtemp()
+        expected = self.write_sample_scripts(source)
+
+        cmd = self.get_build_scripts_cmd(
+            target, [os.path.join(source, fn) for fn in expected]
+        )
+        cmd.finalize_options()
+
+        # https://bugs.python.org/issue4524
+        #
+        # On linux-g++-32 with command line `./configure --enable-ipv6
+        # --with-suffix=3`, python is compiled okay but the build scripts
+        # failed when writing the name of the executable
+        old = sysconfig.get_config_vars().get('VERSION')
+        sysconfig._config_vars['VERSION'] = 4
+        try:
+            cmd.run()
+        finally:
+            if old is not None:
+                sysconfig._config_vars['VERSION'] = old
+
+        built = os.listdir(target)
+        for name in expected:
+            assert name in built
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_check.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_check.py
new file mode 100644
index 00000000..b672b1f9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_check.py
@@ -0,0 +1,194 @@
+"""Tests for distutils.command.check."""
+
+import os
+import textwrap
+from distutils.command.check import check
+from distutils.errors import DistutilsSetupError
+from distutils.tests import support
+
+import pytest
+
+try:
+    import pygments
+except ImportError:
+    pygments = None
+
+
+HERE = os.path.dirname(__file__)
+
+
+@support.combine_markers
+class TestCheck(support.TempdirManager):
+    def _run(self, metadata=None, cwd=None, **options):
+        if metadata is None:
+            metadata = {}
+        if cwd is not None:
+            old_dir = os.getcwd()
+            os.chdir(cwd)
+        pkg_info, dist = self.create_dist(**metadata)
+        cmd = check(dist)
+        cmd.initialize_options()
+        for name, value in options.items():
+            setattr(cmd, name, value)
+        cmd.ensure_finalized()
+        cmd.run()
+        if cwd is not None:
+            os.chdir(old_dir)
+        return cmd
+
+    def test_check_metadata(self):
+        # let's run the command with no metadata at all
+        # by default, check is checking the metadata
+        # should have some warnings
+        cmd = self._run()
+        assert cmd._warnings == 1
+
+        # now let's add the required fields
+        # and run it again, to make sure we don't get
+        # any warning anymore
+        metadata = {
+            'url': 'xxx',
+            'author': 'xxx',
+            'author_email': 'xxx',
+            'name': 'xxx',
+            'version': 'xxx',
+        }
+        cmd = self._run(metadata)
+        assert cmd._warnings == 0
+
+        # now with the strict mode, we should
+        # get an error if there are missing metadata
+        with pytest.raises(DistutilsSetupError):
+            self._run({}, **{'strict': 1})
+
+        # and of course, no error when all metadata are present
+        cmd = self._run(metadata, strict=True)
+        assert cmd._warnings == 0
+
+        # now a test with non-ASCII characters
+        metadata = {
+            'url': 'xxx',
+            'author': '\u00c9ric',
+            'author_email': 'xxx',
+            'name': 'xxx',
+            'version': 'xxx',
+            'description': 'Something about esszet \u00df',
+            'long_description': 'More things about esszet \u00df',
+        }
+        cmd = self._run(metadata)
+        assert cmd._warnings == 0
+
+    def test_check_author_maintainer(self):
+        for kind in ("author", "maintainer"):
+            # ensure no warning when author_email or maintainer_email is given
+            # (the spec allows these fields to take the form "Name <email>")
+            metadata = {
+                'url': 'xxx',
+                kind + '_email': 'Name <name@email.com>',
+                'name': 'xxx',
+                'version': 'xxx',
+            }
+            cmd = self._run(metadata)
+            assert cmd._warnings == 0
+
+            # the check should not warn if only email is given
+            metadata[kind + '_email'] = 'name@email.com'
+            cmd = self._run(metadata)
+            assert cmd._warnings == 0
+
+            # the check should not warn if only the name is given
+            metadata[kind] = "Name"
+            del metadata[kind + '_email']
+            cmd = self._run(metadata)
+            assert cmd._warnings == 0
+
+    def test_check_document(self):
+        pytest.importorskip('docutils')
+        pkg_info, dist = self.create_dist()
+        cmd = check(dist)
+
+        # let's see if it detects broken rest
+        broken_rest = 'title\n===\n\ntest'
+        msgs = cmd._check_rst_data(broken_rest)
+        assert len(msgs) == 1
+
+        # and non-broken rest
+        rest = 'title\n=====\n\ntest'
+        msgs = cmd._check_rst_data(rest)
+        assert len(msgs) == 0
+
+    def test_check_restructuredtext(self):
+        pytest.importorskip('docutils')
+        # let's see if it detects broken rest in long_description
+        broken_rest = 'title\n===\n\ntest'
+        pkg_info, dist = self.create_dist(long_description=broken_rest)
+        cmd = check(dist)
+        cmd.check_restructuredtext()
+        assert cmd._warnings == 1
+
+        # let's see if we have an error with strict=True
+        metadata = {
+            'url': 'xxx',
+            'author': 'xxx',
+            'author_email': 'xxx',
+            'name': 'xxx',
+            'version': 'xxx',
+            'long_description': broken_rest,
+        }
+        with pytest.raises(DistutilsSetupError):
+            self._run(metadata, **{'strict': 1, 'restructuredtext': 1})
+
+        # and non-broken rest, including a non-ASCII character to test #12114
+        metadata['long_description'] = 'title\n=====\n\ntest \u00df'
+        cmd = self._run(metadata, strict=True, restructuredtext=True)
+        assert cmd._warnings == 0
+
+        # check that includes work to test #31292
+        metadata['long_description'] = 'title\n=====\n\n.. include:: includetest.rst'
+        cmd = self._run(metadata, cwd=HERE, strict=True, restructuredtext=True)
+        assert cmd._warnings == 0
+
+    def test_check_restructuredtext_with_syntax_highlight(self):
+        pytest.importorskip('docutils')
+        # Don't fail if there is a `code` or `code-block` directive
+
+        example_rst_docs = [
+            textwrap.dedent(
+                """\
+            Here's some code:
+
+            .. code:: python
+
+                def foo():
+                    pass
+            """
+            ),
+            textwrap.dedent(
+                """\
+            Here's some code:
+
+            .. code-block:: python
+
+                def foo():
+                    pass
+            """
+            ),
+        ]
+
+        for rest_with_code in example_rst_docs:
+            pkg_info, dist = self.create_dist(long_description=rest_with_code)
+            cmd = check(dist)
+            cmd.check_restructuredtext()
+            msgs = cmd._check_rst_data(rest_with_code)
+            if pygments is not None:
+                assert len(msgs) == 0
+            else:
+                assert len(msgs) == 1
+                assert (
+                    str(msgs[0][1])
+                    == 'Cannot analyze code. Pygments package not found.'
+                )
+
+    def test_check_all(self):
+        with pytest.raises(DistutilsSetupError):
+            self._run({}, **{'strict': 1, 'restructuredtext': 1})
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_clean.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_clean.py
new file mode 100644
index 00000000..cc78f30f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_clean.py
@@ -0,0 +1,45 @@
+"""Tests for distutils.command.clean."""
+
+import os
+from distutils.command.clean import clean
+from distutils.tests import support
+
+
+class TestClean(support.TempdirManager):
+    def test_simple_run(self):
+        pkg_dir, dist = self.create_dist()
+        cmd = clean(dist)
+
+        # let's add some elements clean should remove
+        dirs = [
+            (d, os.path.join(pkg_dir, d))
+            for d in (
+                'build_temp',
+                'build_lib',
+                'bdist_base',
+                'build_scripts',
+                'build_base',
+            )
+        ]
+
+        for name, path in dirs:
+            os.mkdir(path)
+            setattr(cmd, name, path)
+            if name == 'build_base':
+                continue
+            for f in ('one', 'two', 'three'):
+                self.write_file(os.path.join(path, f))
+
+        # let's run the command
+        cmd.all = 1
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # make sure the files where removed
+        for _name, path in dirs:
+            assert not os.path.exists(path), f'{path} was not removed'
+
+        # let's run the command again (should spit warnings but succeed)
+        cmd.all = 1
+        cmd.ensure_finalized()
+        cmd.run()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_cmd.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_cmd.py
new file mode 100644
index 00000000..76e8f598
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_cmd.py
@@ -0,0 +1,107 @@
+"""Tests for distutils.cmd."""
+
+import os
+from distutils import debug
+from distutils.cmd import Command
+from distutils.dist import Distribution
+from distutils.errors import DistutilsOptionError
+
+import pytest
+
+
+class MyCmd(Command):
+    def initialize_options(self):
+        pass
+
+
+@pytest.fixture
+def cmd(request):
+    return MyCmd(Distribution())
+
+
+class TestCommand:
+    def test_ensure_string_list(self, cmd):
+        cmd.not_string_list = ['one', 2, 'three']
+        cmd.yes_string_list = ['one', 'two', 'three']
+        cmd.not_string_list2 = object()
+        cmd.yes_string_list2 = 'ok'
+        cmd.ensure_string_list('yes_string_list')
+        cmd.ensure_string_list('yes_string_list2')
+
+        with pytest.raises(DistutilsOptionError):
+            cmd.ensure_string_list('not_string_list')
+
+        with pytest.raises(DistutilsOptionError):
+            cmd.ensure_string_list('not_string_list2')
+
+        cmd.option1 = 'ok,dok'
+        cmd.ensure_string_list('option1')
+        assert cmd.option1 == ['ok', 'dok']
+
+        cmd.option2 = ['xxx', 'www']
+        cmd.ensure_string_list('option2')
+
+        cmd.option3 = ['ok', 2]
+        with pytest.raises(DistutilsOptionError):
+            cmd.ensure_string_list('option3')
+
+    def test_make_file(self, cmd):
+        # making sure it raises when infiles is not a string or a list/tuple
+        with pytest.raises(TypeError):
+            cmd.make_file(infiles=True, outfile='', func='func', args=())
+
+        # making sure execute gets called properly
+        def _execute(func, args, exec_msg, level):
+            assert exec_msg == 'generating out from in'
+
+        cmd.force = True
+        cmd.execute = _execute
+        cmd.make_file(infiles='in', outfile='out', func='func', args=())
+
+    def test_dump_options(self, cmd):
+        msgs = []
+
+        def _announce(msg, level):
+            msgs.append(msg)
+
+        cmd.announce = _announce
+        cmd.option1 = 1
+        cmd.option2 = 1
+        cmd.user_options = [('option1', '', ''), ('option2', '', '')]
+        cmd.dump_options()
+
+        wanted = ["command options for 'MyCmd':", '  option1 = 1', '  option2 = 1']
+        assert msgs == wanted
+
+    def test_ensure_string(self, cmd):
+        cmd.option1 = 'ok'
+        cmd.ensure_string('option1')
+
+        cmd.option2 = None
+        cmd.ensure_string('option2', 'xxx')
+        assert hasattr(cmd, 'option2')
+
+        cmd.option3 = 1
+        with pytest.raises(DistutilsOptionError):
+            cmd.ensure_string('option3')
+
+    def test_ensure_filename(self, cmd):
+        cmd.option1 = __file__
+        cmd.ensure_filename('option1')
+        cmd.option2 = 'xxx'
+        with pytest.raises(DistutilsOptionError):
+            cmd.ensure_filename('option2')
+
+    def test_ensure_dirname(self, cmd):
+        cmd.option1 = os.path.dirname(__file__) or os.curdir
+        cmd.ensure_dirname('option1')
+        cmd.option2 = 'xxx'
+        with pytest.raises(DistutilsOptionError):
+            cmd.ensure_dirname('option2')
+
+    def test_debug_print(self, cmd, capsys, monkeypatch):
+        cmd.debug_print('xxx')
+        assert capsys.readouterr().out == ''
+        monkeypatch.setattr(debug, 'DEBUG', True)
+        cmd.debug_print('xxx')
+        assert capsys.readouterr().out == 'xxx\n'
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_config_cmd.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_config_cmd.py
new file mode 100644
index 00000000..ebee2ef9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_config_cmd.py
@@ -0,0 +1,87 @@
+"""Tests for distutils.command.config."""
+
+import os
+import sys
+from distutils._log import log
+from distutils.command.config import config, dump_file
+from distutils.tests import missing_compiler_executable, support
+
+import more_itertools
+import path
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def info_log(request, monkeypatch):
+    self = request.instance
+    self._logs = []
+    monkeypatch.setattr(log, 'info', self._info)
+
+
+@support.combine_markers
+class TestConfig(support.TempdirManager):
+    def _info(self, msg, *args):
+        for line in msg.splitlines():
+            self._logs.append(line)
+
+    def test_dump_file(self):
+        this_file = path.Path(__file__).with_suffix('.py')
+        with this_file.open(encoding='utf-8') as f:
+            numlines = more_itertools.ilen(f)
+
+        dump_file(this_file, 'I am the header')
+        assert len(self._logs) == numlines + 1
+
+    @pytest.mark.skipif('platform.system() == "Windows"')
+    def test_search_cpp(self):
+        cmd = missing_compiler_executable(['preprocessor'])
+        if cmd is not None:
+            self.skipTest(f'The {cmd!r} command is not found')
+        pkg_dir, dist = self.create_dist()
+        cmd = config(dist)
+        cmd._check_compiler()
+        compiler = cmd.compiler
+        if sys.platform[:3] == "aix" and "xlc" in compiler.preprocessor[0].lower():
+            self.skipTest(
+                'xlc: The -E option overrides the -P, -o, and -qsyntaxonly options'
+            )
+
+        # simple pattern searches
+        match = cmd.search_cpp(pattern='xxx', body='/* xxx */')
+        assert match == 0
+
+        match = cmd.search_cpp(pattern='_configtest', body='/* xxx */')
+        assert match == 1
+
+    def test_finalize_options(self):
+        # finalize_options does a bit of transformation
+        # on options
+        pkg_dir, dist = self.create_dist()
+        cmd = config(dist)
+        cmd.include_dirs = f'one{os.pathsep}two'
+        cmd.libraries = 'one'
+        cmd.library_dirs = f'three{os.pathsep}four'
+        cmd.ensure_finalized()
+
+        assert cmd.include_dirs == ['one', 'two']
+        assert cmd.libraries == ['one']
+        assert cmd.library_dirs == ['three', 'four']
+
+    def test_clean(self):
+        # _clean removes files
+        tmp_dir = self.mkdtemp()
+        f1 = os.path.join(tmp_dir, 'one')
+        f2 = os.path.join(tmp_dir, 'two')
+
+        self.write_file(f1, 'xxx')
+        self.write_file(f2, 'xxx')
+
+        for f in (f1, f2):
+            assert os.path.exists(f)
+
+        pkg_dir, dist = self.create_dist()
+        cmd = config(dist)
+        cmd._clean(f1, f2)
+
+        for f in (f1, f2):
+            assert not os.path.exists(f)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_core.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_core.py
new file mode 100644
index 00000000..bad3fb7e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_core.py
@@ -0,0 +1,130 @@
+"""Tests for distutils.core."""
+
+import distutils.core
+import io
+import os
+import sys
+from distutils.dist import Distribution
+
+import pytest
+
+# setup script that uses __file__
+setup_using___file__ = """\
+
+__file__
+
+from distutils.core import setup
+setup()
+"""
+
+setup_prints_cwd = """\
+
+import os
+print(os.getcwd())
+
+from distutils.core import setup
+setup()
+"""
+
+setup_does_nothing = """\
+from distutils.core import setup
+setup()
+"""
+
+
+setup_defines_subclass = """\
+from distutils.core import setup
+from distutils.command.install import install as _install
+
+class install(_install):
+    sub_commands = _install.sub_commands + ['cmd']
+
+setup(cmdclass={'install': install})
+"""
+
+setup_within_if_main = """\
+from distutils.core import setup
+
+def main():
+    return setup(name="setup_within_if_main")
+
+if __name__ == "__main__":
+    main()
+"""
+
+
+@pytest.fixture(autouse=True)
+def save_stdout(monkeypatch):
+    monkeypatch.setattr(sys, 'stdout', sys.stdout)
+
+
+@pytest.fixture
+def temp_file(tmp_path):
+    return tmp_path / 'file'
+
+
+@pytest.mark.usefixtures('save_env')
+@pytest.mark.usefixtures('save_argv')
+class TestCore:
+    def test_run_setup_provides_file(self, temp_file):
+        # Make sure the script can use __file__; if that's missing, the test
+        # setup.py script will raise NameError.
+        temp_file.write_text(setup_using___file__, encoding='utf-8')
+        distutils.core.run_setup(temp_file)
+
+    def test_run_setup_preserves_sys_argv(self, temp_file):
+        # Make sure run_setup does not clobber sys.argv
+        argv_copy = sys.argv.copy()
+        temp_file.write_text(setup_does_nothing, encoding='utf-8')
+        distutils.core.run_setup(temp_file)
+        assert sys.argv == argv_copy
+
+    def test_run_setup_defines_subclass(self, temp_file):
+        # Make sure the script can use __file__; if that's missing, the test
+        # setup.py script will raise NameError.
+        temp_file.write_text(setup_defines_subclass, encoding='utf-8')
+        dist = distutils.core.run_setup(temp_file)
+        install = dist.get_command_obj('install')
+        assert 'cmd' in install.sub_commands
+
+    def test_run_setup_uses_current_dir(self, tmp_path):
+        """
+        Test that the setup script is run with the current directory
+        as its own current directory.
+        """
+        sys.stdout = io.StringIO()
+        cwd = os.getcwd()
+
+        # Create a directory and write the setup.py file there:
+        setup_py = tmp_path / 'setup.py'
+        setup_py.write_text(setup_prints_cwd, encoding='utf-8')
+        distutils.core.run_setup(setup_py)
+
+        output = sys.stdout.getvalue()
+        if output.endswith("\n"):
+            output = output[:-1]
+        assert cwd == output
+
+    def test_run_setup_within_if_main(self, temp_file):
+        temp_file.write_text(setup_within_if_main, encoding='utf-8')
+        dist = distutils.core.run_setup(temp_file, stop_after="config")
+        assert isinstance(dist, Distribution)
+        assert dist.get_name() == "setup_within_if_main"
+
+    def test_run_commands(self, temp_file):
+        sys.argv = ['setup.py', 'build']
+        temp_file.write_text(setup_within_if_main, encoding='utf-8')
+        dist = distutils.core.run_setup(temp_file, stop_after="commandline")
+        assert 'build' not in dist.have_run
+        distutils.core.run_commands(dist)
+        assert 'build' in dist.have_run
+
+    def test_debug_mode(self, capsys, monkeypatch):
+        # this covers the code called when DEBUG is set
+        sys.argv = ['setup.py', '--name']
+        distutils.core.setup(name='bar')
+        assert capsys.readouterr().out == 'bar\n'
+        monkeypatch.setattr(distutils.core, 'DEBUG', True)
+        distutils.core.setup(name='bar')
+        wanted = "options (after parsing config files):\n"
+        assert capsys.readouterr().out.startswith(wanted)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_dir_util.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_dir_util.py
new file mode 100644
index 00000000..326cb346
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_dir_util.py
@@ -0,0 +1,139 @@
+"""Tests for distutils.dir_util."""
+
+import os
+import pathlib
+import stat
+import sys
+import unittest.mock as mock
+from distutils import dir_util, errors
+from distutils.dir_util import (
+    copy_tree,
+    create_tree,
+    ensure_relative,
+    mkpath,
+    remove_tree,
+)
+from distutils.tests import support
+
+import jaraco.path
+import path
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def stuff(request, monkeypatch, distutils_managed_tempdir):
+    self = request.instance
+    tmp_dir = self.mkdtemp()
+    self.root_target = os.path.join(tmp_dir, 'deep')
+    self.target = os.path.join(self.root_target, 'here')
+    self.target2 = os.path.join(tmp_dir, 'deep2')
+
+
+class TestDirUtil(support.TempdirManager):
+    def test_mkpath_remove_tree_verbosity(self, caplog):
+        mkpath(self.target, verbose=False)
+        assert not caplog.records
+        remove_tree(self.root_target, verbose=False)
+
+        mkpath(self.target, verbose=True)
+        wanted = [f'creating {self.target}']
+        assert caplog.messages == wanted
+        caplog.clear()
+
+        remove_tree(self.root_target, verbose=True)
+        wanted = [f"removing '{self.root_target}' (and everything under it)"]
+        assert caplog.messages == wanted
+
+    @pytest.mark.skipif("platform.system() == 'Windows'")
+    def test_mkpath_with_custom_mode(self):
+        # Get and set the current umask value for testing mode bits.
+        umask = os.umask(0o002)
+        os.umask(umask)
+        mkpath(self.target, 0o700)
+        assert stat.S_IMODE(os.stat(self.target).st_mode) == 0o700 & ~umask
+        mkpath(self.target2, 0o555)
+        assert stat.S_IMODE(os.stat(self.target2).st_mode) == 0o555 & ~umask
+
+    def test_create_tree_verbosity(self, caplog):
+        create_tree(self.root_target, ['one', 'two', 'three'], verbose=False)
+        assert caplog.messages == []
+        remove_tree(self.root_target, verbose=False)
+
+        wanted = [f'creating {self.root_target}']
+        create_tree(self.root_target, ['one', 'two', 'three'], verbose=True)
+        assert caplog.messages == wanted
+
+        remove_tree(self.root_target, verbose=False)
+
+    def test_copy_tree_verbosity(self, caplog):
+        mkpath(self.target, verbose=False)
+
+        copy_tree(self.target, self.target2, verbose=False)
+        assert caplog.messages == []
+
+        remove_tree(self.root_target, verbose=False)
+
+        mkpath(self.target, verbose=False)
+        a_file = path.Path(self.target) / 'ok.txt'
+        jaraco.path.build({'ok.txt': 'some content'}, self.target)
+
+        wanted = [f'copying {a_file} -> {self.target2}']
+        copy_tree(self.target, self.target2, verbose=True)
+        assert caplog.messages == wanted
+
+        remove_tree(self.root_target, verbose=False)
+        remove_tree(self.target2, verbose=False)
+
+    def test_copy_tree_skips_nfs_temp_files(self):
+        mkpath(self.target, verbose=False)
+
+        jaraco.path.build({'ok.txt': 'some content', '.nfs123abc': ''}, self.target)
+
+        copy_tree(self.target, self.target2)
+        assert os.listdir(self.target2) == ['ok.txt']
+
+        remove_tree(self.root_target, verbose=False)
+        remove_tree(self.target2, verbose=False)
+
+    def test_ensure_relative(self):
+        if os.sep == '/':
+            assert ensure_relative('/home/foo') == 'home/foo'
+            assert ensure_relative('some/path') == 'some/path'
+        else:  # \\
+            assert ensure_relative('c:\\home\\foo') == 'c:home\\foo'
+            assert ensure_relative('home\\foo') == 'home\\foo'
+
+    def test_copy_tree_exception_in_listdir(self):
+        """
+        An exception in listdir should raise a DistutilsFileError
+        """
+        with (
+            mock.patch("os.listdir", side_effect=OSError()),
+            pytest.raises(errors.DistutilsFileError),
+        ):
+            src = self.tempdirs[-1]
+            dir_util.copy_tree(src, None)
+
+    def test_mkpath_exception_uncached(self, monkeypatch, tmp_path):
+        """
+        Caching should not remember failed attempts.
+
+        pypa/distutils#304
+        """
+
+        class FailPath(pathlib.Path):
+            def mkdir(self, *args, **kwargs):
+                raise OSError("Failed to create directory")
+
+            if sys.version_info < (3, 12):
+                _flavour = pathlib.Path()._flavour
+
+        target = tmp_path / 'foodir'
+
+        with pytest.raises(errors.DistutilsFileError):
+            mkpath(FailPath(target))
+
+        assert not target.exists()
+
+        mkpath(target)
+        assert target.exists()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_dist.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_dist.py
new file mode 100644
index 00000000..2c5beebe
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_dist.py
@@ -0,0 +1,552 @@
+"""Tests for distutils.dist."""
+
+import email
+import email.generator
+import email.policy
+import functools
+import io
+import os
+import sys
+import textwrap
+import unittest.mock as mock
+import warnings
+from distutils.cmd import Command
+from distutils.dist import Distribution, fix_help_options
+from distutils.tests import support
+from typing import ClassVar
+
+import jaraco.path
+import pytest
+
+pydistutils_cfg = '.' * (os.name == 'posix') + 'pydistutils.cfg'
+
+
+class test_dist(Command):
+    """Sample distutils extension command."""
+
+    user_options: ClassVar[list[tuple[str, str, str]]] = [
+        ("sample-option=", "S", "help text"),
+    ]
+
+    def initialize_options(self):
+        self.sample_option = None
+
+
+class TestDistribution(Distribution):
+    """Distribution subclasses that avoids the default search for
+    configuration files.
+
+    The ._config_files attribute must be set before
+    .parse_config_files() is called.
+    """
+
+    def find_config_files(self):
+        return self._config_files
+
+
+@pytest.fixture
+def clear_argv():
+    del sys.argv[1:]
+
+
+@support.combine_markers
+@pytest.mark.usefixtures('save_env')
+@pytest.mark.usefixtures('save_argv')
+class TestDistributionBehavior(support.TempdirManager):
+    def create_distribution(self, configfiles=()):
+        d = TestDistribution()
+        d._config_files = configfiles
+        d.parse_config_files()
+        d.parse_command_line()
+        return d
+
+    def test_command_packages_unspecified(self, clear_argv):
+        sys.argv.append("build")
+        d = self.create_distribution()
+        assert d.get_command_packages() == ["distutils.command"]
+
+    def test_command_packages_cmdline(self, clear_argv):
+        from distutils.tests.test_dist import test_dist
+
+        sys.argv.extend([
+            "--command-packages",
+            "foo.bar,distutils.tests",
+            "test_dist",
+            "-Ssometext",
+        ])
+        d = self.create_distribution()
+        # let's actually try to load our test command:
+        assert d.get_command_packages() == [
+            "distutils.command",
+            "foo.bar",
+            "distutils.tests",
+        ]
+        cmd = d.get_command_obj("test_dist")
+        assert isinstance(cmd, test_dist)
+        assert cmd.sample_option == "sometext"
+
+    @pytest.mark.skipif(
+        'distutils' not in Distribution.parse_config_files.__module__,
+        reason='Cannot test when virtualenv has monkey-patched Distribution',
+    )
+    def test_venv_install_options(self, tmp_path, clear_argv):
+        sys.argv.append("install")
+        file = str(tmp_path / 'file')
+
+        fakepath = '/somedir'
+
+        jaraco.path.build({
+            file: f"""
+                    [install]
+                    install-base = {fakepath}
+                    install-platbase = {fakepath}
+                    install-lib = {fakepath}
+                    install-platlib = {fakepath}
+                    install-purelib = {fakepath}
+                    install-headers = {fakepath}
+                    install-scripts = {fakepath}
+                    install-data = {fakepath}
+                    prefix = {fakepath}
+                    exec-prefix = {fakepath}
+                    home = {fakepath}
+                    user = {fakepath}
+                    root = {fakepath}
+                    """,
+        })
+
+        # Base case: Not in a Virtual Environment
+        with mock.patch.multiple(sys, prefix='/a', base_prefix='/a'):
+            d = self.create_distribution([file])
+
+        option_tuple = (file, fakepath)
+
+        result_dict = {
+            'install_base': option_tuple,
+            'install_platbase': option_tuple,
+            'install_lib': option_tuple,
+            'install_platlib': option_tuple,
+            'install_purelib': option_tuple,
+            'install_headers': option_tuple,
+            'install_scripts': option_tuple,
+            'install_data': option_tuple,
+            'prefix': option_tuple,
+            'exec_prefix': option_tuple,
+            'home': option_tuple,
+            'user': option_tuple,
+            'root': option_tuple,
+        }
+
+        assert sorted(d.command_options.get('install').keys()) == sorted(
+            result_dict.keys()
+        )
+
+        for key, value in d.command_options.get('install').items():
+            assert value == result_dict[key]
+
+        # Test case: In a Virtual Environment
+        with mock.patch.multiple(sys, prefix='/a', base_prefix='/b'):
+            d = self.create_distribution([file])
+
+        for key in result_dict.keys():
+            assert key not in d.command_options.get('install', {})
+
+    def test_command_packages_configfile(self, tmp_path, clear_argv):
+        sys.argv.append("build")
+        file = str(tmp_path / "file")
+        jaraco.path.build({
+            file: """
+                    [global]
+                    command_packages = foo.bar, splat
+                    """,
+        })
+
+        d = self.create_distribution([file])
+        assert d.get_command_packages() == ["distutils.command", "foo.bar", "splat"]
+
+        # ensure command line overrides config:
+        sys.argv[1:] = ["--command-packages", "spork", "build"]
+        d = self.create_distribution([file])
+        assert d.get_command_packages() == ["distutils.command", "spork"]
+
+        # Setting --command-packages to '' should cause the default to
+        # be used even if a config file specified something else:
+        sys.argv[1:] = ["--command-packages", "", "build"]
+        d = self.create_distribution([file])
+        assert d.get_command_packages() == ["distutils.command"]
+
+    def test_empty_options(self, request):
+        # an empty options dictionary should not stay in the
+        # list of attributes
+
+        # catching warnings
+        warns = []
+
+        def _warn(msg):
+            warns.append(msg)
+
+        request.addfinalizer(
+            functools.partial(setattr, warnings, 'warn', warnings.warn)
+        )
+        warnings.warn = _warn
+        dist = Distribution(
+            attrs={
+                'author': 'xxx',
+                'name': 'xxx',
+                'version': 'xxx',
+                'url': 'xxxx',
+                'options': {},
+            }
+        )
+
+        assert len(warns) == 0
+        assert 'options' not in dir(dist)
+
+    def test_finalize_options(self):
+        attrs = {'keywords': 'one,two', 'platforms': 'one,two'}
+
+        dist = Distribution(attrs=attrs)
+        dist.finalize_options()
+
+        # finalize_option splits platforms and keywords
+        assert dist.metadata.platforms == ['one', 'two']
+        assert dist.metadata.keywords == ['one', 'two']
+
+        attrs = {'keywords': 'foo bar', 'platforms': 'foo bar'}
+        dist = Distribution(attrs=attrs)
+        dist.finalize_options()
+        assert dist.metadata.platforms == ['foo bar']
+        assert dist.metadata.keywords == ['foo bar']
+
+    def test_get_command_packages(self):
+        dist = Distribution()
+        assert dist.command_packages is None
+        cmds = dist.get_command_packages()
+        assert cmds == ['distutils.command']
+        assert dist.command_packages == ['distutils.command']
+
+        dist.command_packages = 'one,two'
+        cmds = dist.get_command_packages()
+        assert cmds == ['distutils.command', 'one', 'two']
+
+    def test_announce(self):
+        # make sure the level is known
+        dist = Distribution()
+        with pytest.raises(TypeError):
+            dist.announce('ok', level='ok2')
+
+    def test_find_config_files_disable(self, temp_home):
+        # Ticket #1180: Allow user to disable their home config file.
+        jaraco.path.build({pydistutils_cfg: '[distutils]\n'}, temp_home)
+
+        d = Distribution()
+        all_files = d.find_config_files()
+
+        d = Distribution(attrs={'script_args': ['--no-user-cfg']})
+        files = d.find_config_files()
+
+        # make sure --no-user-cfg disables the user cfg file
+        assert len(all_files) - 1 == len(files)
+
+    def test_script_args_list_coercion(self):
+        d = Distribution(attrs={'script_args': ('build', '--no-user-cfg')})
+
+        # make sure script_args is a list even if it started as a different iterable
+        assert d.script_args == ['build', '--no-user-cfg']
+
+    @pytest.mark.skipif(
+        'platform.system() == "Windows"',
+        reason='Windows does not honor chmod 000',
+    )
+    def test_find_config_files_permission_error(self, fake_home):
+        """
+        Finding config files should not fail when directory is inaccessible.
+        """
+        fake_home.joinpath(pydistutils_cfg).write_text('', encoding='utf-8')
+        fake_home.chmod(0o000)
+        Distribution().find_config_files()
+
+
+@pytest.mark.usefixtures('save_env')
+@pytest.mark.usefixtures('save_argv')
+class TestMetadata(support.TempdirManager):
+    def format_metadata(self, dist):
+        sio = io.StringIO()
+        dist.metadata.write_pkg_file(sio)
+        return sio.getvalue()
+
+    def test_simple_metadata(self):
+        attrs = {"name": "package", "version": "1.0"}
+        dist = Distribution(attrs)
+        meta = self.format_metadata(dist)
+        assert "Metadata-Version: 1.0" in meta
+        assert "provides:" not in meta.lower()
+        assert "requires:" not in meta.lower()
+        assert "obsoletes:" not in meta.lower()
+
+    def test_provides(self):
+        attrs = {
+            "name": "package",
+            "version": "1.0",
+            "provides": ["package", "package.sub"],
+        }
+        dist = Distribution(attrs)
+        assert dist.metadata.get_provides() == ["package", "package.sub"]
+        assert dist.get_provides() == ["package", "package.sub"]
+        meta = self.format_metadata(dist)
+        assert "Metadata-Version: 1.1" in meta
+        assert "requires:" not in meta.lower()
+        assert "obsoletes:" not in meta.lower()
+
+    def test_provides_illegal(self):
+        with pytest.raises(ValueError):
+            Distribution(
+                {"name": "package", "version": "1.0", "provides": ["my.pkg (splat)"]},
+            )
+
+    def test_requires(self):
+        attrs = {
+            "name": "package",
+            "version": "1.0",
+            "requires": ["other", "another (==1.0)"],
+        }
+        dist = Distribution(attrs)
+        assert dist.metadata.get_requires() == ["other", "another (==1.0)"]
+        assert dist.get_requires() == ["other", "another (==1.0)"]
+        meta = self.format_metadata(dist)
+        assert "Metadata-Version: 1.1" in meta
+        assert "provides:" not in meta.lower()
+        assert "Requires: other" in meta
+        assert "Requires: another (==1.0)" in meta
+        assert "obsoletes:" not in meta.lower()
+
+    def test_requires_illegal(self):
+        with pytest.raises(ValueError):
+            Distribution(
+                {"name": "package", "version": "1.0", "requires": ["my.pkg (splat)"]},
+            )
+
+    def test_requires_to_list(self):
+        attrs = {"name": "package", "requires": iter(["other"])}
+        dist = Distribution(attrs)
+        assert isinstance(dist.metadata.requires, list)
+
+    def test_obsoletes(self):
+        attrs = {
+            "name": "package",
+            "version": "1.0",
+            "obsoletes": ["other", "another (<1.0)"],
+        }
+        dist = Distribution(attrs)
+        assert dist.metadata.get_obsoletes() == ["other", "another (<1.0)"]
+        assert dist.get_obsoletes() == ["other", "another (<1.0)"]
+        meta = self.format_metadata(dist)
+        assert "Metadata-Version: 1.1" in meta
+        assert "provides:" not in meta.lower()
+        assert "requires:" not in meta.lower()
+        assert "Obsoletes: other" in meta
+        assert "Obsoletes: another (<1.0)" in meta
+
+    def test_obsoletes_illegal(self):
+        with pytest.raises(ValueError):
+            Distribution(
+                {"name": "package", "version": "1.0", "obsoletes": ["my.pkg (splat)"]},
+            )
+
+    def test_obsoletes_to_list(self):
+        attrs = {"name": "package", "obsoletes": iter(["other"])}
+        dist = Distribution(attrs)
+        assert isinstance(dist.metadata.obsoletes, list)
+
+    def test_classifier(self):
+        attrs = {
+            'name': 'Boa',
+            'version': '3.0',
+            'classifiers': ['Programming Language :: Python :: 3'],
+        }
+        dist = Distribution(attrs)
+        assert dist.get_classifiers() == ['Programming Language :: Python :: 3']
+        meta = self.format_metadata(dist)
+        assert 'Metadata-Version: 1.1' in meta
+
+    def test_classifier_invalid_type(self, caplog):
+        attrs = {
+            'name': 'Boa',
+            'version': '3.0',
+            'classifiers': ('Programming Language :: Python :: 3',),
+        }
+        d = Distribution(attrs)
+        # should have warning about passing a non-list
+        assert 'should be a list' in caplog.messages[0]
+        # should be converted to a list
+        assert isinstance(d.metadata.classifiers, list)
+        assert d.metadata.classifiers == list(attrs['classifiers'])
+
+    def test_keywords(self):
+        attrs = {
+            'name': 'Monty',
+            'version': '1.0',
+            'keywords': ['spam', 'eggs', 'life of brian'],
+        }
+        dist = Distribution(attrs)
+        assert dist.get_keywords() == ['spam', 'eggs', 'life of brian']
+
+    def test_keywords_invalid_type(self, caplog):
+        attrs = {
+            'name': 'Monty',
+            'version': '1.0',
+            'keywords': ('spam', 'eggs', 'life of brian'),
+        }
+        d = Distribution(attrs)
+        # should have warning about passing a non-list
+        assert 'should be a list' in caplog.messages[0]
+        # should be converted to a list
+        assert isinstance(d.metadata.keywords, list)
+        assert d.metadata.keywords == list(attrs['keywords'])
+
+    def test_platforms(self):
+        attrs = {
+            'name': 'Monty',
+            'version': '1.0',
+            'platforms': ['GNU/Linux', 'Some Evil Platform'],
+        }
+        dist = Distribution(attrs)
+        assert dist.get_platforms() == ['GNU/Linux', 'Some Evil Platform']
+
+    def test_platforms_invalid_types(self, caplog):
+        attrs = {
+            'name': 'Monty',
+            'version': '1.0',
+            'platforms': ('GNU/Linux', 'Some Evil Platform'),
+        }
+        d = Distribution(attrs)
+        # should have warning about passing a non-list
+        assert 'should be a list' in caplog.messages[0]
+        # should be converted to a list
+        assert isinstance(d.metadata.platforms, list)
+        assert d.metadata.platforms == list(attrs['platforms'])
+
+    def test_download_url(self):
+        attrs = {
+            'name': 'Boa',
+            'version': '3.0',
+            'download_url': 'http://example.org/boa',
+        }
+        dist = Distribution(attrs)
+        meta = self.format_metadata(dist)
+        assert 'Metadata-Version: 1.1' in meta
+
+    def test_long_description(self):
+        long_desc = textwrap.dedent(
+            """\
+        example::
+              We start here
+            and continue here
+          and end here."""
+        )
+        attrs = {"name": "package", "version": "1.0", "long_description": long_desc}
+
+        dist = Distribution(attrs)
+        meta = self.format_metadata(dist)
+        meta = meta.replace('\n' + 8 * ' ', '\n')
+        assert long_desc in meta
+
+    def test_custom_pydistutils(self, temp_home):
+        """
+        pydistutils.cfg is found
+        """
+        jaraco.path.build({pydistutils_cfg: ''}, temp_home)
+        config_path = temp_home / pydistutils_cfg
+
+        assert str(config_path) in Distribution().find_config_files()
+
+    def test_extra_pydistutils(self, monkeypatch, tmp_path):
+        jaraco.path.build({'overrides.cfg': ''}, tmp_path)
+        filename = tmp_path / 'overrides.cfg'
+        monkeypatch.setenv('DIST_EXTRA_CONFIG', str(filename))
+        assert str(filename) in Distribution().find_config_files()
+
+    def test_fix_help_options(self):
+        help_tuples = [('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
+        fancy_options = fix_help_options(help_tuples)
+        assert fancy_options[0] == ('a', 'b', 'c')
+        assert fancy_options[1] == (1, 2, 3)
+
+    def test_show_help(self, request, capsys):
+        # smoke test, just makes sure some help is displayed
+        dist = Distribution()
+        sys.argv = []
+        dist.help = True
+        dist.script_name = 'setup.py'
+        dist.parse_command_line()
+
+        output = [
+            line for line in capsys.readouterr().out.split('\n') if line.strip() != ''
+        ]
+        assert output
+
+    def test_read_metadata(self):
+        attrs = {
+            "name": "package",
+            "version": "1.0",
+            "long_description": "desc",
+            "description": "xxx",
+            "download_url": "http://example.com",
+            "keywords": ['one', 'two'],
+            "requires": ['foo'],
+        }
+
+        dist = Distribution(attrs)
+        metadata = dist.metadata
+
+        # write it then reloads it
+        PKG_INFO = io.StringIO()
+        metadata.write_pkg_file(PKG_INFO)
+        PKG_INFO.seek(0)
+        metadata.read_pkg_file(PKG_INFO)
+
+        assert metadata.name == "package"
+        assert metadata.version == "1.0"
+        assert metadata.description == "xxx"
+        assert metadata.download_url == 'http://example.com'
+        assert metadata.keywords == ['one', 'two']
+        assert metadata.platforms is None
+        assert metadata.obsoletes is None
+        assert metadata.requires == ['foo']
+
+    def test_round_trip_through_email_generator(self):
+        """
+        In pypa/setuptools#4033, it was shown that once PKG-INFO is
+        re-generated using ``email.generator.Generator``, some control
+        characters might cause problems.
+        """
+        # Given a PKG-INFO file ...
+        attrs = {
+            "name": "package",
+            "version": "1.0",
+            "long_description": "hello\x0b\nworld\n",
+        }
+        dist = Distribution(attrs)
+        metadata = dist.metadata
+
+        with io.StringIO() as buffer:
+            metadata.write_pkg_file(buffer)
+            msg = buffer.getvalue()
+
+        # ... when it is read and re-written using stdlib's email library,
+        orig = email.message_from_string(msg)
+        policy = email.policy.EmailPolicy(
+            utf8=True,
+            mangle_from_=False,
+            max_line_length=0,
+        )
+        with io.StringIO() as buffer:
+            email.generator.Generator(buffer, policy=policy).flatten(orig)
+
+            buffer.seek(0)
+            regen = email.message_from_file(buffer)
+
+        # ... then it should be the same as the original
+        # (except for the specific line break characters)
+        orig_desc = set(orig["Description"].splitlines())
+        regen_desc = set(regen["Description"].splitlines())
+        assert regen_desc == orig_desc
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_extension.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_extension.py
new file mode 100644
index 00000000..5e8e7682
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_extension.py
@@ -0,0 +1,117 @@
+"""Tests for distutils.extension."""
+
+import os
+import pathlib
+import warnings
+from distutils.extension import Extension, read_setup_file
+
+import pytest
+from test.support.warnings_helper import check_warnings
+
+
+class TestExtension:
+    def test_read_setup_file(self):
+        # trying to read a Setup file
+        # (sample extracted from the PyGame project)
+        setup = os.path.join(os.path.dirname(__file__), 'Setup.sample')
+
+        exts = read_setup_file(setup)
+        names = [ext.name for ext in exts]
+        names.sort()
+
+        # here are the extensions read_setup_file should have created
+        # out of the file
+        wanted = [
+            '_arraysurfarray',
+            '_camera',
+            '_numericsndarray',
+            '_numericsurfarray',
+            'base',
+            'bufferproxy',
+            'cdrom',
+            'color',
+            'constants',
+            'display',
+            'draw',
+            'event',
+            'fastevent',
+            'font',
+            'gfxdraw',
+            'image',
+            'imageext',
+            'joystick',
+            'key',
+            'mask',
+            'mixer',
+            'mixer_music',
+            'mouse',
+            'movie',
+            'overlay',
+            'pixelarray',
+            'pypm',
+            'rect',
+            'rwobject',
+            'scrap',
+            'surface',
+            'surflock',
+            'time',
+            'transform',
+        ]
+
+        assert names == wanted
+
+    def test_extension_init(self):
+        # the first argument, which is the name, must be a string
+        with pytest.raises(TypeError):
+            Extension(1, [])
+        ext = Extension('name', [])
+        assert ext.name == 'name'
+
+        # the second argument, which is the list of files, must
+        # be an iterable of strings or PathLike objects, and not a string
+        with pytest.raises(TypeError):
+            Extension('name', 'file')
+        with pytest.raises(TypeError):
+            Extension('name', ['file', 1])
+        ext = Extension('name', ['file1', 'file2'])
+        assert ext.sources == ['file1', 'file2']
+        ext = Extension('name', [pathlib.Path('file1'), pathlib.Path('file2')])
+        assert ext.sources == ['file1', 'file2']
+
+        # any non-string iterable of strings or PathLike objects should work
+        ext = Extension('name', ('file1', 'file2'))  # tuple
+        assert ext.sources == ['file1', 'file2']
+        ext = Extension('name', {'file1', 'file2'})  # set
+        assert sorted(ext.sources) == ['file1', 'file2']
+        ext = Extension('name', iter(['file1', 'file2']))  # iterator
+        assert ext.sources == ['file1', 'file2']
+        ext = Extension('name', [pathlib.Path('file1'), 'file2'])  # mixed types
+        assert ext.sources == ['file1', 'file2']
+
+        # others arguments have defaults
+        for attr in (
+            'include_dirs',
+            'define_macros',
+            'undef_macros',
+            'library_dirs',
+            'libraries',
+            'runtime_library_dirs',
+            'extra_objects',
+            'extra_compile_args',
+            'extra_link_args',
+            'export_symbols',
+            'swig_opts',
+            'depends',
+        ):
+            assert getattr(ext, attr) == []
+
+        assert ext.language is None
+        assert ext.optional is None
+
+        # if there are unknown keyword options, warn about them
+        with check_warnings() as w:
+            warnings.simplefilter('always')
+            ext = Extension('name', ['file1', 'file2'], chic=True)
+
+        assert len(w.warnings) == 1
+        assert str(w.warnings[0].message) == "Unknown Extension options: 'chic'"
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_file_util.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_file_util.py
new file mode 100644
index 00000000..a75d4a03
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_file_util.py
@@ -0,0 +1,95 @@
+"""Tests for distutils.file_util."""
+
+import errno
+import os
+import unittest.mock as mock
+from distutils.errors import DistutilsFileError
+from distutils.file_util import copy_file, move_file
+
+import jaraco.path
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def stuff(request, tmp_path):
+    self = request.instance
+    self.source = tmp_path / 'f1'
+    self.target = tmp_path / 'f2'
+    self.target_dir = tmp_path / 'd1'
+
+
+class TestFileUtil:
+    def test_move_file_verbosity(self, caplog):
+        jaraco.path.build({self.source: 'some content'})
+
+        move_file(self.source, self.target, verbose=False)
+        assert not caplog.messages
+
+        # back to original state
+        move_file(self.target, self.source, verbose=False)
+
+        move_file(self.source, self.target, verbose=True)
+        wanted = [f'moving {self.source} -> {self.target}']
+        assert caplog.messages == wanted
+
+        # back to original state
+        move_file(self.target, self.source, verbose=False)
+
+        caplog.clear()
+        # now the target is a dir
+        os.mkdir(self.target_dir)
+        move_file(self.source, self.target_dir, verbose=True)
+        wanted = [f'moving {self.source} -> {self.target_dir}']
+        assert caplog.messages == wanted
+
+    def test_move_file_exception_unpacking_rename(self):
+        # see issue 22182
+        with (
+            mock.patch("os.rename", side_effect=OSError("wrong", 1)),
+            pytest.raises(DistutilsFileError),
+        ):
+            jaraco.path.build({self.source: 'spam eggs'})
+            move_file(self.source, self.target, verbose=False)
+
+    def test_move_file_exception_unpacking_unlink(self):
+        # see issue 22182
+        with (
+            mock.patch("os.rename", side_effect=OSError(errno.EXDEV, "wrong")),
+            mock.patch("os.unlink", side_effect=OSError("wrong", 1)),
+            pytest.raises(DistutilsFileError),
+        ):
+            jaraco.path.build({self.source: 'spam eggs'})
+            move_file(self.source, self.target, verbose=False)
+
+    def test_copy_file_hard_link(self):
+        jaraco.path.build({self.source: 'some content'})
+        # Check first that copy_file() will not fall back on copying the file
+        # instead of creating the hard link.
+        try:
+            os.link(self.source, self.target)
+        except OSError as e:
+            self.skipTest(f'os.link: {e}')
+        else:
+            self.target.unlink()
+        st = os.stat(self.source)
+        copy_file(self.source, self.target, link='hard')
+        st2 = os.stat(self.source)
+        st3 = os.stat(self.target)
+        assert os.path.samestat(st, st2), (st, st2)
+        assert os.path.samestat(st2, st3), (st2, st3)
+        assert self.source.read_text(encoding='utf-8') == 'some content'
+
+    def test_copy_file_hard_link_failure(self):
+        # If hard linking fails, copy_file() falls back on copying file
+        # (some special filesystems don't support hard linking even under
+        #  Unix, see issue #8876).
+        jaraco.path.build({self.source: 'some content'})
+        st = os.stat(self.source)
+        with mock.patch("os.link", side_effect=OSError(0, "linking unsupported")):
+            copy_file(self.source, self.target, link='hard')
+        st2 = os.stat(self.source)
+        st3 = os.stat(self.target)
+        assert os.path.samestat(st, st2), (st, st2)
+        assert not os.path.samestat(st2, st3), (st2, st3)
+        for fn in (self.source, self.target):
+            assert fn.read_text(encoding='utf-8') == 'some content'
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_filelist.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_filelist.py
new file mode 100644
index 00000000..130e6fb5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_filelist.py
@@ -0,0 +1,336 @@
+"""Tests for distutils.filelist."""
+
+import logging
+import os
+import re
+from distutils import debug, filelist
+from distutils.errors import DistutilsTemplateError
+from distutils.filelist import FileList, glob_to_re, translate_pattern
+
+import jaraco.path
+import pytest
+
+from .compat import py39 as os_helper
+
+MANIFEST_IN = """\
+include ok
+include xo
+exclude xo
+include foo.tmp
+include buildout.cfg
+global-include *.x
+global-include *.txt
+global-exclude *.tmp
+recursive-include f *.oo
+recursive-exclude global *.x
+graft dir
+prune dir3
+"""
+
+
+def make_local_path(s):
+    """Converts '/' in a string to os.sep"""
+    return s.replace('/', os.sep)
+
+
+class TestFileList:
+    def assertNoWarnings(self, caplog):
+        warnings = [rec for rec in caplog.records if rec.levelno == logging.WARNING]
+        assert not warnings
+        caplog.clear()
+
+    def assertWarnings(self, caplog):
+        warnings = [rec for rec in caplog.records if rec.levelno == logging.WARNING]
+        assert warnings
+        caplog.clear()
+
+    def test_glob_to_re(self):
+        sep = os.sep
+        if os.sep == '\\':
+            sep = re.escape(os.sep)
+
+        for glob, regex in (
+            # simple cases
+            ('foo*', r'(?s:foo[^%(sep)s]*)\Z'),
+            ('foo?', r'(?s:foo[^%(sep)s])\Z'),
+            ('foo??', r'(?s:foo[^%(sep)s][^%(sep)s])\Z'),
+            # special cases
+            (r'foo\\*', r'(?s:foo\\\\[^%(sep)s]*)\Z'),
+            (r'foo\\\*', r'(?s:foo\\\\\\[^%(sep)s]*)\Z'),
+            ('foo????', r'(?s:foo[^%(sep)s][^%(sep)s][^%(sep)s][^%(sep)s])\Z'),
+            (r'foo\\??', r'(?s:foo\\\\[^%(sep)s][^%(sep)s])\Z'),
+        ):
+            regex = regex % {'sep': sep}
+            assert glob_to_re(glob) == regex
+
+    def test_process_template_line(self):
+        # testing  all MANIFEST.in template patterns
+        file_list = FileList()
+        mlp = make_local_path
+
+        # simulated file list
+        file_list.allfiles = [
+            'foo.tmp',
+            'ok',
+            'xo',
+            'four.txt',
+            'buildout.cfg',
+            # filelist does not filter out VCS directories,
+            # it's sdist that does
+            mlp('.hg/last-message.txt'),
+            mlp('global/one.txt'),
+            mlp('global/two.txt'),
+            mlp('global/files.x'),
+            mlp('global/here.tmp'),
+            mlp('f/o/f.oo'),
+            mlp('dir/graft-one'),
+            mlp('dir/dir2/graft2'),
+            mlp('dir3/ok'),
+            mlp('dir3/sub/ok.txt'),
+        ]
+
+        for line in MANIFEST_IN.split('\n'):
+            if line.strip() == '':
+                continue
+            file_list.process_template_line(line)
+
+        wanted = [
+            'ok',
+            'buildout.cfg',
+            'four.txt',
+            mlp('.hg/last-message.txt'),
+            mlp('global/one.txt'),
+            mlp('global/two.txt'),
+            mlp('f/o/f.oo'),
+            mlp('dir/graft-one'),
+            mlp('dir/dir2/graft2'),
+        ]
+
+        assert file_list.files == wanted
+
+    def test_debug_print(self, capsys, monkeypatch):
+        file_list = FileList()
+        file_list.debug_print('xxx')
+        assert capsys.readouterr().out == ''
+
+        monkeypatch.setattr(debug, 'DEBUG', True)
+        file_list.debug_print('xxx')
+        assert capsys.readouterr().out == 'xxx\n'
+
+    def test_set_allfiles(self):
+        file_list = FileList()
+        files = ['a', 'b', 'c']
+        file_list.set_allfiles(files)
+        assert file_list.allfiles == files
+
+    def test_remove_duplicates(self):
+        file_list = FileList()
+        file_list.files = ['a', 'b', 'a', 'g', 'c', 'g']
+        # files must be sorted beforehand (sdist does it)
+        file_list.sort()
+        file_list.remove_duplicates()
+        assert file_list.files == ['a', 'b', 'c', 'g']
+
+    def test_translate_pattern(self):
+        # not regex
+        assert hasattr(translate_pattern('a', anchor=True, is_regex=False), 'search')
+
+        # is a regex
+        regex = re.compile('a')
+        assert translate_pattern(regex, anchor=True, is_regex=True) == regex
+
+        # plain string flagged as regex
+        assert hasattr(translate_pattern('a', anchor=True, is_regex=True), 'search')
+
+        # glob support
+        assert translate_pattern('*.py', anchor=True, is_regex=False).search(
+            'filelist.py'
+        )
+
+    def test_exclude_pattern(self):
+        # return False if no match
+        file_list = FileList()
+        assert not file_list.exclude_pattern('*.py')
+
+        # return True if files match
+        file_list = FileList()
+        file_list.files = ['a.py', 'b.py']
+        assert file_list.exclude_pattern('*.py')
+
+        # test excludes
+        file_list = FileList()
+        file_list.files = ['a.py', 'a.txt']
+        file_list.exclude_pattern('*.py')
+        assert file_list.files == ['a.txt']
+
+    def test_include_pattern(self):
+        # return False if no match
+        file_list = FileList()
+        file_list.set_allfiles([])
+        assert not file_list.include_pattern('*.py')
+
+        # return True if files match
+        file_list = FileList()
+        file_list.set_allfiles(['a.py', 'b.txt'])
+        assert file_list.include_pattern('*.py')
+
+        # test * matches all files
+        file_list = FileList()
+        assert file_list.allfiles is None
+        file_list.set_allfiles(['a.py', 'b.txt'])
+        file_list.include_pattern('*')
+        assert file_list.allfiles == ['a.py', 'b.txt']
+
+    def test_process_template(self, caplog):
+        mlp = make_local_path
+        # invalid lines
+        file_list = FileList()
+        for action in (
+            'include',
+            'exclude',
+            'global-include',
+            'global-exclude',
+            'recursive-include',
+            'recursive-exclude',
+            'graft',
+            'prune',
+            'blarg',
+        ):
+            with pytest.raises(DistutilsTemplateError):
+                file_list.process_template_line(action)
+
+        # include
+        file_list = FileList()
+        file_list.set_allfiles(['a.py', 'b.txt', mlp('d/c.py')])
+
+        file_list.process_template_line('include *.py')
+        assert file_list.files == ['a.py']
+        self.assertNoWarnings(caplog)
+
+        file_list.process_template_line('include *.rb')
+        assert file_list.files == ['a.py']
+        self.assertWarnings(caplog)
+
+        # exclude
+        file_list = FileList()
+        file_list.files = ['a.py', 'b.txt', mlp('d/c.py')]
+
+        file_list.process_template_line('exclude *.py')
+        assert file_list.files == ['b.txt', mlp('d/c.py')]
+        self.assertNoWarnings(caplog)
+
+        file_list.process_template_line('exclude *.rb')
+        assert file_list.files == ['b.txt', mlp('d/c.py')]
+        self.assertWarnings(caplog)
+
+        # global-include
+        file_list = FileList()
+        file_list.set_allfiles(['a.py', 'b.txt', mlp('d/c.py')])
+
+        file_list.process_template_line('global-include *.py')
+        assert file_list.files == ['a.py', mlp('d/c.py')]
+        self.assertNoWarnings(caplog)
+
+        file_list.process_template_line('global-include *.rb')
+        assert file_list.files == ['a.py', mlp('d/c.py')]
+        self.assertWarnings(caplog)
+
+        # global-exclude
+        file_list = FileList()
+        file_list.files = ['a.py', 'b.txt', mlp('d/c.py')]
+
+        file_list.process_template_line('global-exclude *.py')
+        assert file_list.files == ['b.txt']
+        self.assertNoWarnings(caplog)
+
+        file_list.process_template_line('global-exclude *.rb')
+        assert file_list.files == ['b.txt']
+        self.assertWarnings(caplog)
+
+        # recursive-include
+        file_list = FileList()
+        file_list.set_allfiles(['a.py', mlp('d/b.py'), mlp('d/c.txt'), mlp('d/d/e.py')])
+
+        file_list.process_template_line('recursive-include d *.py')
+        assert file_list.files == [mlp('d/b.py'), mlp('d/d/e.py')]
+        self.assertNoWarnings(caplog)
+
+        file_list.process_template_line('recursive-include e *.py')
+        assert file_list.files == [mlp('d/b.py'), mlp('d/d/e.py')]
+        self.assertWarnings(caplog)
+
+        # recursive-exclude
+        file_list = FileList()
+        file_list.files = ['a.py', mlp('d/b.py'), mlp('d/c.txt'), mlp('d/d/e.py')]
+
+        file_list.process_template_line('recursive-exclude d *.py')
+        assert file_list.files == ['a.py', mlp('d/c.txt')]
+        self.assertNoWarnings(caplog)
+
+        file_list.process_template_line('recursive-exclude e *.py')
+        assert file_list.files == ['a.py', mlp('d/c.txt')]
+        self.assertWarnings(caplog)
+
+        # graft
+        file_list = FileList()
+        file_list.set_allfiles(['a.py', mlp('d/b.py'), mlp('d/d/e.py'), mlp('f/f.py')])
+
+        file_list.process_template_line('graft d')
+        assert file_list.files == [mlp('d/b.py'), mlp('d/d/e.py')]
+        self.assertNoWarnings(caplog)
+
+        file_list.process_template_line('graft e')
+        assert file_list.files == [mlp('d/b.py'), mlp('d/d/e.py')]
+        self.assertWarnings(caplog)
+
+        # prune
+        file_list = FileList()
+        file_list.files = ['a.py', mlp('d/b.py'), mlp('d/d/e.py'), mlp('f/f.py')]
+
+        file_list.process_template_line('prune d')
+        assert file_list.files == ['a.py', mlp('f/f.py')]
+        self.assertNoWarnings(caplog)
+
+        file_list.process_template_line('prune e')
+        assert file_list.files == ['a.py', mlp('f/f.py')]
+        self.assertWarnings(caplog)
+
+
+class TestFindAll:
+    @os_helper.skip_unless_symlink
+    def test_missing_symlink(self, temp_cwd):
+        os.symlink('foo', 'bar')
+        assert filelist.findall() == []
+
+    def test_basic_discovery(self, temp_cwd):
+        """
+        When findall is called with no parameters or with
+        '.' as the parameter, the dot should be omitted from
+        the results.
+        """
+        jaraco.path.build({'foo': {'file1.txt': ''}, 'bar': {'file2.txt': ''}})
+        file1 = os.path.join('foo', 'file1.txt')
+        file2 = os.path.join('bar', 'file2.txt')
+        expected = [file2, file1]
+        assert sorted(filelist.findall()) == expected
+
+    def test_non_local_discovery(self, tmp_path):
+        """
+        When findall is called with another path, the full
+        path name should be returned.
+        """
+        jaraco.path.build({'file1.txt': ''}, tmp_path)
+        expected = [str(tmp_path / 'file1.txt')]
+        assert filelist.findall(tmp_path) == expected
+
+    @os_helper.skip_unless_symlink
+    def test_symlink_loop(self, tmp_path):
+        jaraco.path.build(
+            {
+                'link-to-parent': jaraco.path.Symlink('.'),
+                'somefile': '',
+            },
+            tmp_path,
+        )
+        files = filelist.findall(tmp_path)
+        assert len(files) == 1
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install.py
new file mode 100644
index 00000000..b3ffb2e6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install.py
@@ -0,0 +1,245 @@
+"""Tests for distutils.command.install."""
+
+import logging
+import os
+import pathlib
+import site
+import sys
+from distutils import sysconfig
+from distutils.command import install as install_module
+from distutils.command.build_ext import build_ext
+from distutils.command.install import INSTALL_SCHEMES, install
+from distutils.core import Distribution
+from distutils.errors import DistutilsOptionError
+from distutils.extension import Extension
+from distutils.tests import missing_compiler_executable, support
+from distutils.util import is_mingw
+
+import pytest
+
+
+def _make_ext_name(modname):
+    return modname + sysconfig.get_config_var('EXT_SUFFIX')
+
+
+@support.combine_markers
+@pytest.mark.usefixtures('save_env')
+class TestInstall(
+    support.TempdirManager,
+):
+    @pytest.mark.xfail(
+        'platform.system() == "Windows" and sys.version_info > (3, 11)',
+        reason="pypa/distutils#148",
+    )
+    def test_home_installation_scheme(self):
+        # This ensure two things:
+        # - that --home generates the desired set of directory names
+        # - test --home is supported on all platforms
+        builddir = self.mkdtemp()
+        destination = os.path.join(builddir, "installation")
+
+        dist = Distribution({"name": "foopkg"})
+        # script_name need not exist, it just need to be initialized
+        dist.script_name = os.path.join(builddir, "setup.py")
+        dist.command_obj["build"] = support.DummyCommand(
+            build_base=builddir,
+            build_lib=os.path.join(builddir, "lib"),
+        )
+
+        cmd = install(dist)
+        cmd.home = destination
+        cmd.ensure_finalized()
+
+        assert cmd.install_base == destination
+        assert cmd.install_platbase == destination
+
+        def check_path(got, expected):
+            got = os.path.normpath(got)
+            expected = os.path.normpath(expected)
+            assert got == expected
+
+        impl_name = sys.implementation.name.replace("cpython", "python")
+        libdir = os.path.join(destination, "lib", impl_name)
+        check_path(cmd.install_lib, libdir)
+        _platlibdir = getattr(sys, "platlibdir", "lib")
+        platlibdir = os.path.join(destination, _platlibdir, impl_name)
+        check_path(cmd.install_platlib, platlibdir)
+        check_path(cmd.install_purelib, libdir)
+        check_path(
+            cmd.install_headers,
+            os.path.join(destination, "include", impl_name, "foopkg"),
+        )
+        check_path(cmd.install_scripts, os.path.join(destination, "bin"))
+        check_path(cmd.install_data, destination)
+
+    def test_user_site(self, monkeypatch):
+        # test install with --user
+        # preparing the environment for the test
+        self.tmpdir = self.mkdtemp()
+        orig_site = site.USER_SITE
+        orig_base = site.USER_BASE
+        monkeypatch.setattr(site, 'USER_BASE', os.path.join(self.tmpdir, 'B'))
+        monkeypatch.setattr(site, 'USER_SITE', os.path.join(self.tmpdir, 'S'))
+        monkeypatch.setattr(install_module, 'USER_BASE', site.USER_BASE)
+        monkeypatch.setattr(install_module, 'USER_SITE', site.USER_SITE)
+
+        def _expanduser(path):
+            if path.startswith('~'):
+                return os.path.normpath(self.tmpdir + path[1:])
+            return path
+
+        monkeypatch.setattr(os.path, 'expanduser', _expanduser)
+
+        for key in ('nt_user', 'posix_user'):
+            assert key in INSTALL_SCHEMES
+
+        dist = Distribution({'name': 'xx'})
+        cmd = install(dist)
+
+        # making sure the user option is there
+        options = [name for name, short, label in cmd.user_options]
+        assert 'user' in options
+
+        # setting a value
+        cmd.user = True
+
+        # user base and site shouldn't be created yet
+        assert not os.path.exists(site.USER_BASE)
+        assert not os.path.exists(site.USER_SITE)
+
+        # let's run finalize
+        cmd.ensure_finalized()
+
+        # now they should
+        assert os.path.exists(site.USER_BASE)
+        assert os.path.exists(site.USER_SITE)
+
+        assert 'userbase' in cmd.config_vars
+        assert 'usersite' in cmd.config_vars
+
+        actual_headers = os.path.relpath(cmd.install_headers, site.USER_BASE)
+        if os.name == 'nt' and not is_mingw():
+            site_path = os.path.relpath(os.path.dirname(orig_site), orig_base)
+            include = os.path.join(site_path, 'Include')
+        else:
+            include = sysconfig.get_python_inc(0, '')
+        expect_headers = os.path.join(include, 'xx')
+
+        assert os.path.normcase(actual_headers) == os.path.normcase(expect_headers)
+
+    def test_handle_extra_path(self):
+        dist = Distribution({'name': 'xx', 'extra_path': 'path,dirs'})
+        cmd = install(dist)
+
+        # two elements
+        cmd.handle_extra_path()
+        assert cmd.extra_path == ['path', 'dirs']
+        assert cmd.extra_dirs == 'dirs'
+        assert cmd.path_file == 'path'
+
+        # one element
+        cmd.extra_path = ['path']
+        cmd.handle_extra_path()
+        assert cmd.extra_path == ['path']
+        assert cmd.extra_dirs == 'path'
+        assert cmd.path_file == 'path'
+
+        # none
+        dist.extra_path = cmd.extra_path = None
+        cmd.handle_extra_path()
+        assert cmd.extra_path is None
+        assert cmd.extra_dirs == ''
+        assert cmd.path_file is None
+
+        # three elements (no way !)
+        cmd.extra_path = 'path,dirs,again'
+        with pytest.raises(DistutilsOptionError):
+            cmd.handle_extra_path()
+
+    def test_finalize_options(self):
+        dist = Distribution({'name': 'xx'})
+        cmd = install(dist)
+
+        # must supply either prefix/exec-prefix/home or
+        # install-base/install-platbase -- not both
+        cmd.prefix = 'prefix'
+        cmd.install_base = 'base'
+        with pytest.raises(DistutilsOptionError):
+            cmd.finalize_options()
+
+        # must supply either home or prefix/exec-prefix -- not both
+        cmd.install_base = None
+        cmd.home = 'home'
+        with pytest.raises(DistutilsOptionError):
+            cmd.finalize_options()
+
+        # can't combine user with prefix/exec_prefix/home or
+        # install_(plat)base
+        cmd.prefix = None
+        cmd.user = 'user'
+        with pytest.raises(DistutilsOptionError):
+            cmd.finalize_options()
+
+    def test_record(self):
+        install_dir = self.mkdtemp()
+        project_dir, dist = self.create_dist(py_modules=['hello'], scripts=['sayhi'])
+        os.chdir(project_dir)
+        self.write_file('hello.py', "def main(): print('o hai')")
+        self.write_file('sayhi', 'from hello import main; main()')
+
+        cmd = install(dist)
+        dist.command_obj['install'] = cmd
+        cmd.root = install_dir
+        cmd.record = os.path.join(project_dir, 'filelist')
+        cmd.ensure_finalized()
+        cmd.run()
+
+        content = pathlib.Path(cmd.record).read_text(encoding='utf-8')
+
+        found = [pathlib.Path(line).name for line in content.splitlines()]
+        expected = [
+            'hello.py',
+            f'hello.{sys.implementation.cache_tag}.pyc',
+            'sayhi',
+            'UNKNOWN-0.0.0-py{}.{}.egg-info'.format(*sys.version_info[:2]),
+        ]
+        assert found == expected
+
+    def test_record_extensions(self):
+        cmd = missing_compiler_executable()
+        if cmd is not None:
+            pytest.skip(f'The {cmd!r} command is not found')
+        install_dir = self.mkdtemp()
+        project_dir, dist = self.create_dist(
+            ext_modules=[Extension('xx', ['xxmodule.c'])]
+        )
+        os.chdir(project_dir)
+        support.copy_xxmodule_c(project_dir)
+
+        buildextcmd = build_ext(dist)
+        support.fixup_build_ext(buildextcmd)
+        buildextcmd.ensure_finalized()
+
+        cmd = install(dist)
+        dist.command_obj['install'] = cmd
+        dist.command_obj['build_ext'] = buildextcmd
+        cmd.root = install_dir
+        cmd.record = os.path.join(project_dir, 'filelist')
+        cmd.ensure_finalized()
+        cmd.run()
+
+        content = pathlib.Path(cmd.record).read_text(encoding='utf-8')
+
+        found = [pathlib.Path(line).name for line in content.splitlines()]
+        expected = [
+            _make_ext_name('xx'),
+            'UNKNOWN-0.0.0-py{}.{}.egg-info'.format(*sys.version_info[:2]),
+        ]
+        assert found == expected
+
+    def test_debug_mode(self, caplog, monkeypatch):
+        # this covers the code called when DEBUG is set
+        monkeypatch.setattr(install_module, 'DEBUG', True)
+        caplog.set_level(logging.DEBUG)
+        self.test_record()
+        assert any(rec for rec in caplog.records if rec.levelno == logging.DEBUG)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_data.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_data.py
new file mode 100644
index 00000000..c800f86c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_data.py
@@ -0,0 +1,74 @@
+"""Tests for distutils.command.install_data."""
+
+import os
+import pathlib
+from distutils.command.install_data import install_data
+from distutils.tests import support
+
+import pytest
+
+
+@pytest.mark.usefixtures('save_env')
+class TestInstallData(
+    support.TempdirManager,
+):
+    def test_simple_run(self):
+        pkg_dir, dist = self.create_dist()
+        cmd = install_data(dist)
+        cmd.install_dir = inst = os.path.join(pkg_dir, 'inst')
+
+        # data_files can contain
+        #  - simple files
+        #  - a Path object
+        #  - a tuple with a path, and a list of file
+        one = os.path.join(pkg_dir, 'one')
+        self.write_file(one, 'xxx')
+        inst2 = os.path.join(pkg_dir, 'inst2')
+        two = os.path.join(pkg_dir, 'two')
+        self.write_file(two, 'xxx')
+        three = pathlib.Path(pkg_dir) / 'three'
+        self.write_file(three, 'xxx')
+
+        cmd.data_files = [one, (inst2, [two]), three]
+        assert cmd.get_inputs() == [one, (inst2, [two]), three]
+
+        # let's run the command
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # let's check the result
+        assert len(cmd.get_outputs()) == 3
+        rthree = os.path.split(one)[-1]
+        assert os.path.exists(os.path.join(inst, rthree))
+        rtwo = os.path.split(two)[-1]
+        assert os.path.exists(os.path.join(inst2, rtwo))
+        rone = os.path.split(one)[-1]
+        assert os.path.exists(os.path.join(inst, rone))
+        cmd.outfiles = []
+
+        # let's try with warn_dir one
+        cmd.warn_dir = True
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # let's check the result
+        assert len(cmd.get_outputs()) == 3
+        assert os.path.exists(os.path.join(inst, rthree))
+        assert os.path.exists(os.path.join(inst2, rtwo))
+        assert os.path.exists(os.path.join(inst, rone))
+        cmd.outfiles = []
+
+        # now using root and empty dir
+        cmd.root = os.path.join(pkg_dir, 'root')
+        inst5 = os.path.join(pkg_dir, 'inst5')
+        four = os.path.join(cmd.install_dir, 'four')
+        self.write_file(four, 'xx')
+        cmd.data_files = [one, (inst2, [two]), three, ('inst5', [four]), (inst5, [])]
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # let's check the result
+        assert len(cmd.get_outputs()) == 5
+        assert os.path.exists(os.path.join(inst, rthree))
+        assert os.path.exists(os.path.join(inst2, rtwo))
+        assert os.path.exists(os.path.join(inst, rone))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_headers.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_headers.py
new file mode 100644
index 00000000..2c74f06b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_headers.py
@@ -0,0 +1,33 @@
+"""Tests for distutils.command.install_headers."""
+
+import os
+from distutils.command.install_headers import install_headers
+from distutils.tests import support
+
+import pytest
+
+
+@pytest.mark.usefixtures('save_env')
+class TestInstallHeaders(
+    support.TempdirManager,
+):
+    def test_simple_run(self):
+        # we have two headers
+        header_list = self.mkdtemp()
+        header1 = os.path.join(header_list, 'header1')
+        header2 = os.path.join(header_list, 'header2')
+        self.write_file(header1)
+        self.write_file(header2)
+        headers = [header1, header2]
+
+        pkg_dir, dist = self.create_dist(headers=headers)
+        cmd = install_headers(dist)
+        assert cmd.get_inputs() == headers
+
+        # let's run the command
+        cmd.install_dir = os.path.join(pkg_dir, 'inst')
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # let's check the results
+        assert len(cmd.get_outputs()) == 2
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_lib.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_lib.py
new file mode 100644
index 00000000..f685a579
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_lib.py
@@ -0,0 +1,110 @@
+"""Tests for distutils.command.install_data."""
+
+import importlib.util
+import os
+import sys
+from distutils.command.install_lib import install_lib
+from distutils.errors import DistutilsOptionError
+from distutils.extension import Extension
+from distutils.tests import support
+
+import pytest
+
+
+@support.combine_markers
+@pytest.mark.usefixtures('save_env')
+class TestInstallLib(
+    support.TempdirManager,
+):
+    def test_finalize_options(self):
+        dist = self.create_dist()[1]
+        cmd = install_lib(dist)
+
+        cmd.finalize_options()
+        assert cmd.compile == 1
+        assert cmd.optimize == 0
+
+        # optimize must be 0, 1, or 2
+        cmd.optimize = 'foo'
+        with pytest.raises(DistutilsOptionError):
+            cmd.finalize_options()
+        cmd.optimize = '4'
+        with pytest.raises(DistutilsOptionError):
+            cmd.finalize_options()
+
+        cmd.optimize = '2'
+        cmd.finalize_options()
+        assert cmd.optimize == 2
+
+    @pytest.mark.skipif('sys.dont_write_bytecode')
+    def test_byte_compile(self):
+        project_dir, dist = self.create_dist()
+        os.chdir(project_dir)
+        cmd = install_lib(dist)
+        cmd.compile = cmd.optimize = 1
+
+        f = os.path.join(project_dir, 'foo.py')
+        self.write_file(f, '# python file')
+        cmd.byte_compile([f])
+        pyc_file = importlib.util.cache_from_source('foo.py', optimization='')
+        pyc_opt_file = importlib.util.cache_from_source(
+            'foo.py', optimization=cmd.optimize
+        )
+        assert os.path.exists(pyc_file)
+        assert os.path.exists(pyc_opt_file)
+
+    def test_get_outputs(self):
+        project_dir, dist = self.create_dist()
+        os.chdir(project_dir)
+        os.mkdir('spam')
+        cmd = install_lib(dist)
+
+        # setting up a dist environment
+        cmd.compile = cmd.optimize = 1
+        cmd.install_dir = self.mkdtemp()
+        f = os.path.join(project_dir, 'spam', '__init__.py')
+        self.write_file(f, '# python package')
+        cmd.distribution.ext_modules = [Extension('foo', ['xxx'])]
+        cmd.distribution.packages = ['spam']
+        cmd.distribution.script_name = 'setup.py'
+
+        # get_outputs should return 4 elements: spam/__init__.py and .pyc,
+        # foo.import-tag-abiflags.so / foo.pyd
+        outputs = cmd.get_outputs()
+        assert len(outputs) == 4, outputs
+
+    def test_get_inputs(self):
+        project_dir, dist = self.create_dist()
+        os.chdir(project_dir)
+        os.mkdir('spam')
+        cmd = install_lib(dist)
+
+        # setting up a dist environment
+        cmd.compile = cmd.optimize = 1
+        cmd.install_dir = self.mkdtemp()
+        f = os.path.join(project_dir, 'spam', '__init__.py')
+        self.write_file(f, '# python package')
+        cmd.distribution.ext_modules = [Extension('foo', ['xxx'])]
+        cmd.distribution.packages = ['spam']
+        cmd.distribution.script_name = 'setup.py'
+
+        # get_inputs should return 2 elements: spam/__init__.py and
+        # foo.import-tag-abiflags.so / foo.pyd
+        inputs = cmd.get_inputs()
+        assert len(inputs) == 2, inputs
+
+    def test_dont_write_bytecode(self, caplog):
+        # makes sure byte_compile is not used
+        dist = self.create_dist()[1]
+        cmd = install_lib(dist)
+        cmd.compile = True
+        cmd.optimize = 1
+
+        old_dont_write_bytecode = sys.dont_write_bytecode
+        sys.dont_write_bytecode = True
+        try:
+            cmd.byte_compile([])
+        finally:
+            sys.dont_write_bytecode = old_dont_write_bytecode
+
+        assert 'byte-compiling is disabled' in caplog.messages[0]
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_scripts.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_scripts.py
new file mode 100644
index 00000000..868b1c22
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_install_scripts.py
@@ -0,0 +1,52 @@
+"""Tests for distutils.command.install_scripts."""
+
+import os
+from distutils.command.install_scripts import install_scripts
+from distutils.core import Distribution
+from distutils.tests import support
+
+from . import test_build_scripts
+
+
+class TestInstallScripts(support.TempdirManager):
+    def test_default_settings(self):
+        dist = Distribution()
+        dist.command_obj["build"] = support.DummyCommand(build_scripts="/foo/bar")
+        dist.command_obj["install"] = support.DummyCommand(
+            install_scripts="/splat/funk",
+            force=True,
+            skip_build=True,
+        )
+        cmd = install_scripts(dist)
+        assert not cmd.force
+        assert not cmd.skip_build
+        assert cmd.build_dir is None
+        assert cmd.install_dir is None
+
+        cmd.finalize_options()
+
+        assert cmd.force
+        assert cmd.skip_build
+        assert cmd.build_dir == "/foo/bar"
+        assert cmd.install_dir == "/splat/funk"
+
+    def test_installation(self):
+        source = self.mkdtemp()
+
+        expected = test_build_scripts.TestBuildScripts.write_sample_scripts(source)
+
+        target = self.mkdtemp()
+        dist = Distribution()
+        dist.command_obj["build"] = support.DummyCommand(build_scripts=source)
+        dist.command_obj["install"] = support.DummyCommand(
+            install_scripts=target,
+            force=True,
+            skip_build=True,
+        )
+        cmd = install_scripts(dist)
+        cmd.finalize_options()
+        cmd.run()
+
+        installed = os.listdir(target)
+        for name in expected:
+            assert name in installed
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_log.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_log.py
new file mode 100644
index 00000000..d67779fc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_log.py
@@ -0,0 +1,12 @@
+"""Tests for distutils.log"""
+
+import logging
+from distutils._log import log
+
+
+class TestLog:
+    def test_non_ascii(self, caplog):
+        caplog.set_level(logging.DEBUG)
+        log.debug('Dεbug\tMėssãge')
+        log.fatal('Fαtal\tÈrrōr')
+        assert caplog.messages == ['Dεbug\tMėssãge', 'Fαtal\tÈrrōr']
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_modified.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_modified.py
new file mode 100644
index 00000000..e35cec2d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_modified.py
@@ -0,0 +1,126 @@
+"""Tests for distutils._modified."""
+
+import os
+import types
+from distutils._modified import newer, newer_group, newer_pairwise, newer_pairwise_group
+from distutils.errors import DistutilsFileError
+from distutils.tests import support
+
+import pytest
+
+
+class TestDepUtil(support.TempdirManager):
+    def test_newer(self):
+        tmpdir = self.mkdtemp()
+        new_file = os.path.join(tmpdir, 'new')
+        old_file = os.path.abspath(__file__)
+
+        # Raise DistutilsFileError if 'new_file' does not exist.
+        with pytest.raises(DistutilsFileError):
+            newer(new_file, old_file)
+
+        # Return true if 'new_file' exists and is more recently modified than
+        # 'old_file', or if 'new_file' exists and 'old_file' doesn't.
+        self.write_file(new_file)
+        assert newer(new_file, 'I_dont_exist')
+        assert newer(new_file, old_file)
+
+        # Return false if both exist and 'old_file' is the same age or younger
+        # than 'new_file'.
+        assert not newer(old_file, new_file)
+
+    def _setup_1234(self):
+        tmpdir = self.mkdtemp()
+        sources = os.path.join(tmpdir, 'sources')
+        targets = os.path.join(tmpdir, 'targets')
+        os.mkdir(sources)
+        os.mkdir(targets)
+        one = os.path.join(sources, 'one')
+        two = os.path.join(sources, 'two')
+        three = os.path.abspath(__file__)  # I am the old file
+        four = os.path.join(targets, 'four')
+        self.write_file(one)
+        self.write_file(two)
+        self.write_file(four)
+        return one, two, three, four
+
+    def test_newer_pairwise(self):
+        one, two, three, four = self._setup_1234()
+
+        assert newer_pairwise([one, two], [three, four]) == ([one], [three])
+
+    def test_newer_pairwise_mismatch(self):
+        one, two, three, four = self._setup_1234()
+
+        with pytest.raises(ValueError):
+            newer_pairwise([one], [three, four])
+
+        with pytest.raises(ValueError):
+            newer_pairwise([one, two], [three])
+
+    def test_newer_pairwise_empty(self):
+        assert newer_pairwise([], []) == ([], [])
+
+    def test_newer_pairwise_fresh(self):
+        one, two, three, four = self._setup_1234()
+
+        assert newer_pairwise([one, three], [two, four]) == ([], [])
+
+    def test_newer_group(self):
+        tmpdir = self.mkdtemp()
+        sources = os.path.join(tmpdir, 'sources')
+        os.mkdir(sources)
+        one = os.path.join(sources, 'one')
+        two = os.path.join(sources, 'two')
+        three = os.path.join(sources, 'three')
+        old_file = os.path.abspath(__file__)
+
+        # return true if 'old_file' is out-of-date with respect to any file
+        # listed in 'sources'.
+        self.write_file(one)
+        self.write_file(two)
+        self.write_file(three)
+        assert newer_group([one, two, three], old_file)
+        assert not newer_group([one, two, old_file], three)
+
+        # missing handling
+        os.remove(one)
+        with pytest.raises(OSError):
+            newer_group([one, two, old_file], three)
+
+        assert not newer_group([one, two, old_file], three, missing='ignore')
+
+        assert newer_group([one, two, old_file], three, missing='newer')
+
+
+@pytest.fixture
+def groups_target(tmp_path):
+    """
+    Set up some older sources, a target, and newer sources.
+
+    Returns a simple namespace with these values.
+    """
+    filenames = ['older.c', 'older.h', 'target.o', 'newer.c', 'newer.h']
+    paths = [tmp_path / name for name in filenames]
+
+    for mtime, path in enumerate(paths):
+        path.write_text('', encoding='utf-8')
+
+        # make sure modification times are sequential
+        os.utime(path, (mtime, mtime))
+
+    return types.SimpleNamespace(older=paths[:2], target=paths[2], newer=paths[3:])
+
+
+def test_newer_pairwise_group(groups_target):
+    older = newer_pairwise_group([groups_target.older], [groups_target.target])
+    newer = newer_pairwise_group([groups_target.newer], [groups_target.target])
+    assert older == ([], [])
+    assert newer == ([groups_target.newer], [groups_target.target])
+
+
+def test_newer_group_no_sources_no_target(tmp_path):
+    """
+    Consider no sources and no target "newer".
+    """
+    assert newer_group([], str(tmp_path / 'does-not-exist'))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_sdist.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_sdist.py
new file mode 100644
index 00000000..6b1a376b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_sdist.py
@@ -0,0 +1,470 @@
+"""Tests for distutils.command.sdist."""
+
+import os
+import pathlib
+import shutil  # noqa: F401
+import tarfile
+import zipfile
+from distutils.archive_util import ARCHIVE_FORMATS
+from distutils.command.sdist import sdist, show_formats
+from distutils.core import Distribution
+from distutils.errors import DistutilsOptionError
+from distutils.filelist import FileList
+from os.path import join
+from textwrap import dedent
+
+import jaraco.path
+import path
+import pytest
+from more_itertools import ilen
+
+from . import support
+from .unix_compat import grp, pwd, require_uid_0, require_unix_id
+
+SETUP_PY = """
+from distutils.core import setup
+import somecode
+
+setup(name='fake')
+"""
+
+MANIFEST = """\
+# file GENERATED by distutils, do NOT edit
+README
+buildout.cfg
+inroot.txt
+setup.py
+data%(sep)sdata.dt
+scripts%(sep)sscript.py
+some%(sep)sfile.txt
+some%(sep)sother_file.txt
+somecode%(sep)s__init__.py
+somecode%(sep)sdoc.dat
+somecode%(sep)sdoc.txt
+"""
+
+
+@pytest.fixture(autouse=True)
+def project_dir(request, distutils_managed_tempdir):
+    self = request.instance
+    self.tmp_dir = self.mkdtemp()
+    jaraco.path.build(
+        {
+            'somecode': {
+                '__init__.py': '#',
+            },
+            'README': 'xxx',
+            'setup.py': SETUP_PY,
+        },
+        self.tmp_dir,
+    )
+    with path.Path(self.tmp_dir):
+        yield
+
+
+def clean_lines(filepath):
+    with pathlib.Path(filepath).open(encoding='utf-8') as f:
+        yield from filter(None, map(str.strip, f))
+
+
+class TestSDist(support.TempdirManager):
+    def get_cmd(self, metadata=None):
+        """Returns a cmd"""
+        if metadata is None:
+            metadata = {
+                'name': 'ns.fake--pkg',
+                'version': '1.0',
+                'url': 'xxx',
+                'author': 'xxx',
+                'author_email': 'xxx',
+            }
+        dist = Distribution(metadata)
+        dist.script_name = 'setup.py'
+        dist.packages = ['somecode']
+        dist.include_package_data = True
+        cmd = sdist(dist)
+        cmd.dist_dir = 'dist'
+        return dist, cmd
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_prune_file_list(self):
+        # this test creates a project with some VCS dirs and an NFS rename
+        # file, then launches sdist to check they get pruned on all systems
+
+        # creating VCS directories with some files in them
+        os.mkdir(join(self.tmp_dir, 'somecode', '.svn'))
+        self.write_file((self.tmp_dir, 'somecode', '.svn', 'ok.py'), 'xxx')
+
+        os.mkdir(join(self.tmp_dir, 'somecode', '.hg'))
+        self.write_file((self.tmp_dir, 'somecode', '.hg', 'ok'), 'xxx')
+
+        os.mkdir(join(self.tmp_dir, 'somecode', '.git'))
+        self.write_file((self.tmp_dir, 'somecode', '.git', 'ok'), 'xxx')
+
+        self.write_file((self.tmp_dir, 'somecode', '.nfs0001'), 'xxx')
+
+        # now building a sdist
+        dist, cmd = self.get_cmd()
+
+        # zip is available universally
+        # (tar might not be installed under win32)
+        cmd.formats = ['zip']
+
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # now let's check what we have
+        dist_folder = join(self.tmp_dir, 'dist')
+        files = os.listdir(dist_folder)
+        assert files == ['ns_fake_pkg-1.0.zip']
+
+        zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.0.zip'))
+        try:
+            content = zip_file.namelist()
+        finally:
+            zip_file.close()
+
+        # making sure everything has been pruned correctly
+        expected = [
+            '',
+            'PKG-INFO',
+            'README',
+            'setup.py',
+            'somecode/',
+            'somecode/__init__.py',
+        ]
+        assert sorted(content) == ['ns_fake_pkg-1.0/' + x for x in expected]
+
+    @pytest.mark.usefixtures('needs_zlib')
+    @pytest.mark.skipif("not shutil.which('tar')")
+    @pytest.mark.skipif("not shutil.which('gzip')")
+    def test_make_distribution(self):
+        # now building a sdist
+        dist, cmd = self.get_cmd()
+
+        # creating a gztar then a tar
+        cmd.formats = ['gztar', 'tar']
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # making sure we have two files
+        dist_folder = join(self.tmp_dir, 'dist')
+        result = os.listdir(dist_folder)
+        result.sort()
+        assert result == ['ns_fake_pkg-1.0.tar', 'ns_fake_pkg-1.0.tar.gz']
+
+        os.remove(join(dist_folder, 'ns_fake_pkg-1.0.tar'))
+        os.remove(join(dist_folder, 'ns_fake_pkg-1.0.tar.gz'))
+
+        # now trying a tar then a gztar
+        cmd.formats = ['tar', 'gztar']
+
+        cmd.ensure_finalized()
+        cmd.run()
+
+        result = os.listdir(dist_folder)
+        result.sort()
+        assert result == ['ns_fake_pkg-1.0.tar', 'ns_fake_pkg-1.0.tar.gz']
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_add_defaults(self):
+        # https://bugs.python.org/issue2279
+
+        # add_default should also include
+        # data_files and package_data
+        dist, cmd = self.get_cmd()
+
+        # filling data_files by pointing files
+        # in package_data
+        dist.package_data = {'': ['*.cfg', '*.dat'], 'somecode': ['*.txt']}
+        self.write_file((self.tmp_dir, 'somecode', 'doc.txt'), '#')
+        self.write_file((self.tmp_dir, 'somecode', 'doc.dat'), '#')
+
+        # adding some data in data_files
+        data_dir = join(self.tmp_dir, 'data')
+        os.mkdir(data_dir)
+        self.write_file((data_dir, 'data.dt'), '#')
+        some_dir = join(self.tmp_dir, 'some')
+        os.mkdir(some_dir)
+        # make sure VCS directories are pruned (#14004)
+        hg_dir = join(self.tmp_dir, '.hg')
+        os.mkdir(hg_dir)
+        self.write_file((hg_dir, 'last-message.txt'), '#')
+        # a buggy regex used to prevent this from working on windows (#6884)
+        self.write_file((self.tmp_dir, 'buildout.cfg'), '#')
+        self.write_file((self.tmp_dir, 'inroot.txt'), '#')
+        self.write_file((some_dir, 'file.txt'), '#')
+        self.write_file((some_dir, 'other_file.txt'), '#')
+
+        dist.data_files = [
+            ('data', ['data/data.dt', 'buildout.cfg', 'inroot.txt', 'notexisting']),
+            'some/file.txt',
+            'some/other_file.txt',
+        ]
+
+        # adding a script
+        script_dir = join(self.tmp_dir, 'scripts')
+        os.mkdir(script_dir)
+        self.write_file((script_dir, 'script.py'), '#')
+        dist.scripts = [join('scripts', 'script.py')]
+
+        cmd.formats = ['zip']
+        cmd.use_defaults = True
+
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # now let's check what we have
+        dist_folder = join(self.tmp_dir, 'dist')
+        files = os.listdir(dist_folder)
+        assert files == ['ns_fake_pkg-1.0.zip']
+
+        zip_file = zipfile.ZipFile(join(dist_folder, 'ns_fake_pkg-1.0.zip'))
+        try:
+            content = zip_file.namelist()
+        finally:
+            zip_file.close()
+
+        # making sure everything was added
+        expected = [
+            '',
+            'PKG-INFO',
+            'README',
+            'buildout.cfg',
+            'data/',
+            'data/data.dt',
+            'inroot.txt',
+            'scripts/',
+            'scripts/script.py',
+            'setup.py',
+            'some/',
+            'some/file.txt',
+            'some/other_file.txt',
+            'somecode/',
+            'somecode/__init__.py',
+            'somecode/doc.dat',
+            'somecode/doc.txt',
+        ]
+        assert sorted(content) == ['ns_fake_pkg-1.0/' + x for x in expected]
+
+        # checking the MANIFEST
+        manifest = pathlib.Path(self.tmp_dir, 'MANIFEST').read_text(encoding='utf-8')
+        assert manifest == MANIFEST % {'sep': os.sep}
+
+    @staticmethod
+    def warnings(messages, prefix='warning: '):
+        return [msg for msg in messages if msg.startswith(prefix)]
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_metadata_check_option(self, caplog):
+        # testing the `medata-check` option
+        dist, cmd = self.get_cmd(metadata={})
+
+        # this should raise some warnings !
+        # with the `check` subcommand
+        cmd.ensure_finalized()
+        cmd.run()
+        assert len(self.warnings(caplog.messages, 'warning: check: ')) == 1
+
+        # trying with a complete set of metadata
+        caplog.clear()
+        dist, cmd = self.get_cmd()
+        cmd.ensure_finalized()
+        cmd.metadata_check = False
+        cmd.run()
+        assert len(self.warnings(caplog.messages, 'warning: check: ')) == 0
+
+    def test_show_formats(self, capsys):
+        show_formats()
+
+        # the output should be a header line + one line per format
+        num_formats = len(ARCHIVE_FORMATS.keys())
+        output = [
+            line
+            for line in capsys.readouterr().out.split('\n')
+            if line.strip().startswith('--formats=')
+        ]
+        assert len(output) == num_formats
+
+    def test_finalize_options(self):
+        dist, cmd = self.get_cmd()
+        cmd.finalize_options()
+
+        # default options set by finalize
+        assert cmd.manifest == 'MANIFEST'
+        assert cmd.template == 'MANIFEST.in'
+        assert cmd.dist_dir == 'dist'
+
+        # formats has to be a string splitable on (' ', ',') or
+        # a stringlist
+        cmd.formats = 1
+        with pytest.raises(DistutilsOptionError):
+            cmd.finalize_options()
+        cmd.formats = ['zip']
+        cmd.finalize_options()
+
+        # formats has to be known
+        cmd.formats = 'supazipa'
+        with pytest.raises(DistutilsOptionError):
+            cmd.finalize_options()
+
+    # the following tests make sure there is a nice error message instead
+    # of a traceback when parsing an invalid manifest template
+
+    def _check_template(self, content, caplog):
+        dist, cmd = self.get_cmd()
+        os.chdir(self.tmp_dir)
+        self.write_file('MANIFEST.in', content)
+        cmd.ensure_finalized()
+        cmd.filelist = FileList()
+        cmd.read_template()
+        assert len(self.warnings(caplog.messages)) == 1
+
+    def test_invalid_template_unknown_command(self, caplog):
+        self._check_template('taunt knights *', caplog)
+
+    def test_invalid_template_wrong_arguments(self, caplog):
+        # this manifest command takes one argument
+        self._check_template('prune', caplog)
+
+    @pytest.mark.skipif("platform.system() != 'Windows'")
+    def test_invalid_template_wrong_path(self, caplog):
+        # on Windows, trailing slashes are not allowed
+        # this used to crash instead of raising a warning: #8286
+        self._check_template('include examples/', caplog)
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_get_file_list(self):
+        # make sure MANIFEST is recalculated
+        dist, cmd = self.get_cmd()
+
+        # filling data_files by pointing files in package_data
+        dist.package_data = {'somecode': ['*.txt']}
+        self.write_file((self.tmp_dir, 'somecode', 'doc.txt'), '#')
+        cmd.formats = ['gztar']
+        cmd.ensure_finalized()
+        cmd.run()
+
+        assert ilen(clean_lines(cmd.manifest)) == 5
+
+        # adding a file
+        self.write_file((self.tmp_dir, 'somecode', 'doc2.txt'), '#')
+
+        # make sure build_py is reinitialized, like a fresh run
+        build_py = dist.get_command_obj('build_py')
+        build_py.finalized = False
+        build_py.ensure_finalized()
+
+        cmd.run()
+
+        manifest2 = list(clean_lines(cmd.manifest))
+
+        # do we have the new file in MANIFEST ?
+        assert len(manifest2) == 6
+        assert 'doc2.txt' in manifest2[-1]
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_manifest_marker(self):
+        # check that autogenerated MANIFESTs have a marker
+        dist, cmd = self.get_cmd()
+        cmd.ensure_finalized()
+        cmd.run()
+
+        assert (
+            next(clean_lines(cmd.manifest))
+            == '# file GENERATED by distutils, do NOT edit'
+        )
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_manifest_comments(self):
+        # make sure comments don't cause exceptions or wrong includes
+        contents = dedent(
+            """\
+            # bad.py
+            #bad.py
+            good.py
+            """
+        )
+        dist, cmd = self.get_cmd()
+        cmd.ensure_finalized()
+        self.write_file((self.tmp_dir, cmd.manifest), contents)
+        self.write_file((self.tmp_dir, 'good.py'), '# pick me!')
+        self.write_file((self.tmp_dir, 'bad.py'), "# don't pick me!")
+        self.write_file((self.tmp_dir, '#bad.py'), "# don't pick me!")
+        cmd.run()
+        assert cmd.filelist.files == ['good.py']
+
+    @pytest.mark.usefixtures('needs_zlib')
+    def test_manual_manifest(self):
+        # check that a MANIFEST without a marker is left alone
+        dist, cmd = self.get_cmd()
+        cmd.formats = ['gztar']
+        cmd.ensure_finalized()
+        self.write_file((self.tmp_dir, cmd.manifest), 'README.manual')
+        self.write_file(
+            (self.tmp_dir, 'README.manual'),
+            'This project maintains its MANIFEST file itself.',
+        )
+        cmd.run()
+        assert cmd.filelist.files == ['README.manual']
+
+        assert list(clean_lines(cmd.manifest)) == ['README.manual']
+
+        archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.0.tar.gz')
+        archive = tarfile.open(archive_name)
+        try:
+            filenames = [tarinfo.name for tarinfo in archive]
+        finally:
+            archive.close()
+        assert sorted(filenames) == [
+            'ns_fake_pkg-1.0',
+            'ns_fake_pkg-1.0/PKG-INFO',
+            'ns_fake_pkg-1.0/README.manual',
+        ]
+
+    @pytest.mark.usefixtures('needs_zlib')
+    @require_unix_id
+    @require_uid_0
+    @pytest.mark.skipif("not shutil.which('tar')")
+    @pytest.mark.skipif("not shutil.which('gzip')")
+    def test_make_distribution_owner_group(self):
+        # now building a sdist
+        dist, cmd = self.get_cmd()
+
+        # creating a gztar and specifying the owner+group
+        cmd.formats = ['gztar']
+        cmd.owner = pwd.getpwuid(0)[0]
+        cmd.group = grp.getgrgid(0)[0]
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # making sure we have the good rights
+        archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.0.tar.gz')
+        archive = tarfile.open(archive_name)
+        try:
+            for member in archive.getmembers():
+                assert member.uid == 0
+                assert member.gid == 0
+        finally:
+            archive.close()
+
+        # building a sdist again
+        dist, cmd = self.get_cmd()
+
+        # creating a gztar
+        cmd.formats = ['gztar']
+        cmd.ensure_finalized()
+        cmd.run()
+
+        # making sure we have the good rights
+        archive_name = join(self.tmp_dir, 'dist', 'ns_fake_pkg-1.0.tar.gz')
+        archive = tarfile.open(archive_name)
+
+        # note that we are not testing the group ownership here
+        # because, depending on the platforms and the container
+        # rights (see #7408)
+        try:
+            for member in archive.getmembers():
+                assert member.uid == os.getuid()
+        finally:
+            archive.close()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_spawn.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_spawn.py
new file mode 100644
index 00000000..3b9fc926
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_spawn.py
@@ -0,0 +1,141 @@
+"""Tests for distutils.spawn."""
+
+import os
+import stat
+import sys
+import unittest.mock as mock
+from distutils.errors import DistutilsExecError
+from distutils.spawn import find_executable, spawn
+from distutils.tests import support
+
+import path
+import pytest
+from test.support import unix_shell
+
+from .compat import py39 as os_helper
+
+
+class TestSpawn(support.TempdirManager):
+    @pytest.mark.skipif("os.name not in ('nt', 'posix')")
+    def test_spawn(self):
+        tmpdir = self.mkdtemp()
+
+        # creating something executable
+        # through the shell that returns 1
+        if sys.platform != 'win32':
+            exe = os.path.join(tmpdir, 'foo.sh')
+            self.write_file(exe, f'#!{unix_shell}\nexit 1')
+        else:
+            exe = os.path.join(tmpdir, 'foo.bat')
+            self.write_file(exe, 'exit 1')
+
+        os.chmod(exe, 0o777)
+        with pytest.raises(DistutilsExecError):
+            spawn([exe])
+
+        # now something that works
+        if sys.platform != 'win32':
+            exe = os.path.join(tmpdir, 'foo.sh')
+            self.write_file(exe, f'#!{unix_shell}\nexit 0')
+        else:
+            exe = os.path.join(tmpdir, 'foo.bat')
+            self.write_file(exe, 'exit 0')
+
+        os.chmod(exe, 0o777)
+        spawn([exe])  # should work without any error
+
+    def test_find_executable(self, tmp_path):
+        program_path = self._make_executable(tmp_path, '.exe')
+        program = program_path.name
+        program_noeext = program_path.with_suffix('').name
+        filename = str(program_path)
+        tmp_dir = path.Path(tmp_path)
+
+        # test path parameter
+        rv = find_executable(program, path=tmp_dir)
+        assert rv == filename
+
+        if sys.platform == 'win32':
+            # test without ".exe" extension
+            rv = find_executable(program_noeext, path=tmp_dir)
+            assert rv == filename
+
+        # test find in the current directory
+        with tmp_dir:
+            rv = find_executable(program)
+            assert rv == program
+
+        # test non-existent program
+        dont_exist_program = "dontexist_" + program
+        rv = find_executable(dont_exist_program, path=tmp_dir)
+        assert rv is None
+
+        # PATH='': no match, except in the current directory
+        with os_helper.EnvironmentVarGuard() as env:
+            env['PATH'] = ''
+            with (
+                mock.patch(
+                    'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
+                ),
+                mock.patch('distutils.spawn.os.defpath', tmp_dir),
+            ):
+                rv = find_executable(program)
+                assert rv is None
+
+                # look in current directory
+                with tmp_dir:
+                    rv = find_executable(program)
+                    assert rv == program
+
+        # PATH=':': explicitly looks in the current directory
+        with os_helper.EnvironmentVarGuard() as env:
+            env['PATH'] = os.pathsep
+            with (
+                mock.patch('distutils.spawn.os.confstr', return_value='', create=True),
+                mock.patch('distutils.spawn.os.defpath', ''),
+            ):
+                rv = find_executable(program)
+                assert rv is None
+
+                # look in current directory
+                with tmp_dir:
+                    rv = find_executable(program)
+                    assert rv == program
+
+        # missing PATH: test os.confstr("CS_PATH") and os.defpath
+        with os_helper.EnvironmentVarGuard() as env:
+            env.pop('PATH', None)
+
+            # without confstr
+            with (
+                mock.patch(
+                    'distutils.spawn.os.confstr', side_effect=ValueError, create=True
+                ),
+                mock.patch('distutils.spawn.os.defpath', tmp_dir),
+            ):
+                rv = find_executable(program)
+                assert rv == filename
+
+            # with confstr
+            with (
+                mock.patch(
+                    'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
+                ),
+                mock.patch('distutils.spawn.os.defpath', ''),
+            ):
+                rv = find_executable(program)
+                assert rv == filename
+
+    @staticmethod
+    def _make_executable(tmp_path, ext):
+        # Give the temporary program a suffix regardless of platform.
+        # It's needed on Windows and not harmful on others.
+        program = tmp_path.joinpath('program').with_suffix(ext)
+        program.write_text("", encoding='utf-8')
+        program.chmod(stat.S_IXUSR)
+        return program
+
+    def test_spawn_missing_exe(self):
+        with pytest.raises(DistutilsExecError) as ctx:
+            spawn(['does-not-exist'])
+        assert "command 'does-not-exist' failed" in str(ctx.value)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_sysconfig.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_sysconfig.py
new file mode 100644
index 00000000..43d77c23
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_sysconfig.py
@@ -0,0 +1,319 @@
+"""Tests for distutils.sysconfig."""
+
+import contextlib
+import distutils
+import os
+import pathlib
+import subprocess
+import sys
+from distutils import sysconfig
+from distutils.ccompiler import new_compiler  # noqa: F401
+from distutils.unixccompiler import UnixCCompiler
+
+import jaraco.envs
+import path
+import pytest
+from jaraco.text import trim
+from test.support import swap_item
+
+
+def _gen_makefile(root, contents):
+    jaraco.path.build({'Makefile': trim(contents)}, root)
+    return root / 'Makefile'
+
+
+@pytest.mark.usefixtures('save_env')
+class TestSysconfig:
+    def test_get_config_h_filename(self):
+        config_h = sysconfig.get_config_h_filename()
+        assert os.path.isfile(config_h)
+
+    @pytest.mark.skipif("platform.system() == 'Windows'")
+    @pytest.mark.skipif("sys.implementation.name != 'cpython'")
+    def test_get_makefile_filename(self):
+        makefile = sysconfig.get_makefile_filename()
+        assert os.path.isfile(makefile)
+
+    def test_get_python_lib(self, tmp_path):
+        assert sysconfig.get_python_lib() != sysconfig.get_python_lib(prefix=tmp_path)
+
+    def test_get_config_vars(self):
+        cvars = sysconfig.get_config_vars()
+        assert isinstance(cvars, dict)
+        assert cvars
+
+    @pytest.mark.skipif('sysconfig.IS_PYPY')
+    @pytest.mark.skipif('sysconfig.python_build')
+    @pytest.mark.xfail('platform.system() == "Windows"')
+    def test_srcdir_simple(self):
+        # See #15364.
+        srcdir = pathlib.Path(sysconfig.get_config_var('srcdir'))
+
+        assert srcdir.absolute()
+        assert srcdir.is_dir()
+
+        makefile = pathlib.Path(sysconfig.get_makefile_filename())
+        assert makefile.parent.samefile(srcdir)
+
+    @pytest.mark.skipif('sysconfig.IS_PYPY')
+    @pytest.mark.skipif('not sysconfig.python_build')
+    def test_srcdir_python_build(self):
+        # See #15364.
+        srcdir = pathlib.Path(sysconfig.get_config_var('srcdir'))
+
+        # The python executable has not been installed so srcdir
+        # should be a full source checkout.
+        Python_h = srcdir.joinpath('Include', 'Python.h')
+        assert Python_h.is_file()
+        assert sysconfig._is_python_source_dir(srcdir)
+        assert sysconfig._is_python_source_dir(str(srcdir))
+
+    def test_srcdir_independent_of_cwd(self):
+        """
+        srcdir should be independent of the current working directory
+        """
+        # See #15364.
+        srcdir = sysconfig.get_config_var('srcdir')
+        with path.Path('..'):
+            srcdir2 = sysconfig.get_config_var('srcdir')
+        assert srcdir == srcdir2
+
+    def customize_compiler(self):
+        # make sure AR gets caught
+        class compiler:
+            compiler_type = 'unix'
+            executables = UnixCCompiler.executables
+
+            def __init__(self):
+                self.exes = {}
+
+            def set_executables(self, **kw):
+                for k, v in kw.items():
+                    self.exes[k] = v
+
+        sysconfig_vars = {
+            'AR': 'sc_ar',
+            'CC': 'sc_cc',
+            'CXX': 'sc_cxx',
+            'ARFLAGS': '--sc-arflags',
+            'CFLAGS': '--sc-cflags',
+            'CCSHARED': '--sc-ccshared',
+            'LDSHARED': 'sc_ldshared',
+            'SHLIB_SUFFIX': 'sc_shutil_suffix',
+        }
+
+        comp = compiler()
+        with contextlib.ExitStack() as cm:
+            for key, value in sysconfig_vars.items():
+                cm.enter_context(swap_item(sysconfig._config_vars, key, value))
+            sysconfig.customize_compiler(comp)
+
+        return comp
+
+    @pytest.mark.skipif("not isinstance(new_compiler(), UnixCCompiler)")
+    @pytest.mark.usefixtures('disable_macos_customization')
+    def test_customize_compiler(self):
+        # Make sure that sysconfig._config_vars is initialized
+        sysconfig.get_config_vars()
+
+        os.environ['AR'] = 'env_ar'
+        os.environ['CC'] = 'env_cc'
+        os.environ['CPP'] = 'env_cpp'
+        os.environ['CXX'] = 'env_cxx --env-cxx-flags'
+        os.environ['LDSHARED'] = 'env_ldshared'
+        os.environ['LDFLAGS'] = '--env-ldflags'
+        os.environ['ARFLAGS'] = '--env-arflags'
+        os.environ['CFLAGS'] = '--env-cflags'
+        os.environ['CPPFLAGS'] = '--env-cppflags'
+        os.environ['RANLIB'] = 'env_ranlib'
+
+        comp = self.customize_compiler()
+        assert comp.exes['archiver'] == 'env_ar --env-arflags'
+        assert comp.exes['preprocessor'] == 'env_cpp --env-cppflags'
+        assert comp.exes['compiler'] == 'env_cc --env-cflags --env-cppflags'
+        assert comp.exes['compiler_so'] == (
+            'env_cc --env-cflags --env-cppflags --sc-ccshared'
+        )
+        assert (
+            comp.exes['compiler_cxx']
+            == 'env_cxx --env-cxx-flags --sc-cflags --env-cppflags'
+        )
+        assert comp.exes['linker_exe'] == 'env_cc'
+        assert comp.exes['linker_so'] == (
+            'env_ldshared --env-ldflags --env-cflags --env-cppflags'
+        )
+        assert comp.shared_lib_extension == 'sc_shutil_suffix'
+
+        if sys.platform == "darwin":
+            assert comp.exes['ranlib'] == 'env_ranlib'
+        else:
+            assert 'ranlib' not in comp.exes
+
+        del os.environ['AR']
+        del os.environ['CC']
+        del os.environ['CPP']
+        del os.environ['CXX']
+        del os.environ['LDSHARED']
+        del os.environ['LDFLAGS']
+        del os.environ['ARFLAGS']
+        del os.environ['CFLAGS']
+        del os.environ['CPPFLAGS']
+        del os.environ['RANLIB']
+
+        comp = self.customize_compiler()
+        assert comp.exes['archiver'] == 'sc_ar --sc-arflags'
+        assert comp.exes['preprocessor'] == 'sc_cc -E'
+        assert comp.exes['compiler'] == 'sc_cc --sc-cflags'
+        assert comp.exes['compiler_so'] == 'sc_cc --sc-cflags --sc-ccshared'
+        assert comp.exes['compiler_cxx'] == 'sc_cxx --sc-cflags'
+        assert comp.exes['linker_exe'] == 'sc_cc'
+        assert comp.exes['linker_so'] == 'sc_ldshared'
+        assert comp.shared_lib_extension == 'sc_shutil_suffix'
+        assert 'ranlib' not in comp.exes
+
+    def test_parse_makefile_base(self, tmp_path):
+        makefile = _gen_makefile(
+            tmp_path,
+            """
+            CONFIG_ARGS=  '--arg1=optarg1' 'ENV=LIB'
+            VAR=$OTHER
+            OTHER=foo
+            """,
+        )
+        d = sysconfig.parse_makefile(makefile)
+        assert d == {'CONFIG_ARGS': "'--arg1=optarg1' 'ENV=LIB'", 'OTHER': 'foo'}
+
+    def test_parse_makefile_literal_dollar(self, tmp_path):
+        makefile = _gen_makefile(
+            tmp_path,
+            """
+            CONFIG_ARGS=  '--arg1=optarg1' 'ENV=\\$$LIB'
+            VAR=$OTHER
+            OTHER=foo
+            """,
+        )
+        d = sysconfig.parse_makefile(makefile)
+        assert d == {'CONFIG_ARGS': r"'--arg1=optarg1' 'ENV=\$LIB'", 'OTHER': 'foo'}
+
+    def test_sysconfig_module(self):
+        import sysconfig as global_sysconfig
+
+        assert global_sysconfig.get_config_var('CFLAGS') == sysconfig.get_config_var(
+            'CFLAGS'
+        )
+        assert global_sysconfig.get_config_var('LDFLAGS') == sysconfig.get_config_var(
+            'LDFLAGS'
+        )
+
+    # On macOS, binary installers support extension module building on
+    # various levels of the operating system with differing Xcode
+    # configurations, requiring customization of some of the
+    # compiler configuration directives to suit the environment on
+    # the installed machine. Some of these customizations may require
+    # running external programs and are thus deferred until needed by
+    # the first extension module build. Only
+    # the Distutils version of sysconfig is used for extension module
+    # builds, which happens earlier in the Distutils tests. This may
+    # cause the following tests to fail since no tests have caused
+    # the global version of sysconfig to call the customization yet.
+    # The solution for now is to simply skip this test in this case.
+    # The longer-term solution is to only have one version of sysconfig.
+    @pytest.mark.skipif("sysconfig.get_config_var('CUSTOMIZED_OSX_COMPILER')")
+    def test_sysconfig_compiler_vars(self):
+        import sysconfig as global_sysconfig
+
+        if sysconfig.get_config_var('CUSTOMIZED_OSX_COMPILER'):
+            pytest.skip('compiler flags customized')
+        assert global_sysconfig.get_config_var('LDSHARED') == sysconfig.get_config_var(
+            'LDSHARED'
+        )
+        assert global_sysconfig.get_config_var('CC') == sysconfig.get_config_var('CC')
+
+    @pytest.mark.skipif("not sysconfig.get_config_var('EXT_SUFFIX')")
+    def test_SO_deprecation(self):
+        with pytest.warns(DeprecationWarning):
+            sysconfig.get_config_var('SO')
+
+    def test_customize_compiler_before_get_config_vars(self, tmp_path):
+        # Issue #21923: test that a Distribution compiler
+        # instance can be called without an explicit call to
+        # get_config_vars().
+        jaraco.path.build(
+            {
+                'file': trim("""
+                    from distutils.core import Distribution
+                    config = Distribution().get_command_obj('config')
+                    # try_compile may pass or it may fail if no compiler
+                    # is found but it should not raise an exception.
+                    rc = config.try_compile('int x;')
+                    """)
+            },
+            tmp_path,
+        )
+        p = subprocess.Popen(
+            [sys.executable, tmp_path / 'file'],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            universal_newlines=True,
+            encoding='utf-8',
+        )
+        outs, errs = p.communicate()
+        assert 0 == p.returncode, "Subprocess failed: " + outs
+
+    def test_parse_config_h(self):
+        config_h = sysconfig.get_config_h_filename()
+        input = {}
+        with open(config_h, encoding="utf-8") as f:
+            result = sysconfig.parse_config_h(f, g=input)
+        assert input is result
+        with open(config_h, encoding="utf-8") as f:
+            result = sysconfig.parse_config_h(f)
+        assert isinstance(result, dict)
+
+    @pytest.mark.skipif("platform.system() != 'Windows'")
+    @pytest.mark.skipif("sys.implementation.name != 'cpython'")
+    def test_win_ext_suffix(self):
+        assert sysconfig.get_config_var("EXT_SUFFIX").endswith(".pyd")
+        assert sysconfig.get_config_var("EXT_SUFFIX") != ".pyd"
+
+    @pytest.mark.skipif("platform.system() != 'Windows'")
+    @pytest.mark.skipif("sys.implementation.name != 'cpython'")
+    @pytest.mark.skipif(
+        '\\PCbuild\\'.casefold() not in sys.executable.casefold(),
+        reason='Need sys.executable to be in a source tree',
+    )
+    def test_win_build_venv_from_source_tree(self, tmp_path):
+        """Ensure distutils.sysconfig detects venvs from source tree builds."""
+        env = jaraco.envs.VEnv()
+        env.create_opts = env.clean_opts
+        env.root = tmp_path
+        env.ensure_env()
+        cmd = [
+            env.exe(),
+            "-c",
+            "import distutils.sysconfig; print(distutils.sysconfig.python_build)",
+        ]
+        distutils_path = os.path.dirname(os.path.dirname(distutils.__file__))
+        out = subprocess.check_output(
+            cmd, env={**os.environ, "PYTHONPATH": distutils_path}
+        )
+        assert out == "True"
+
+    def test_get_python_inc_missing_config_dir(self, monkeypatch):
+        """
+        In portable Python installations, the sysconfig will be broken,
+        pointing to the directories where the installation was built and
+        not where it currently is. In this case, ensure that the missing
+        directory isn't used for get_python_inc.
+
+        See pypa/distutils#178.
+        """
+
+        def override(name):
+            if name == 'INCLUDEPY':
+                return '/does-not-exist'
+            return sysconfig.get_config_var(name)
+
+        monkeypatch.setattr(sysconfig, 'get_config_var', override)
+
+        assert os.path.exists(sysconfig.get_python_inc())
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_text_file.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_text_file.py
new file mode 100644
index 00000000..f5111565
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_text_file.py
@@ -0,0 +1,127 @@
+"""Tests for distutils.text_file."""
+
+from distutils.tests import support
+from distutils.text_file import TextFile
+
+import jaraco.path
+import path
+
+TEST_DATA = """# test file
+
+line 3 \\
+# intervening comment
+  continues on next line
+"""
+
+
+class TestTextFile(support.TempdirManager):
+    def test_class(self):
+        # old tests moved from text_file.__main__
+        # so they are really called by the buildbots
+
+        # result 1: no fancy options
+        result1 = [
+            '# test file\n',
+            '\n',
+            'line 3 \\\n',
+            '# intervening comment\n',
+            '  continues on next line\n',
+        ]
+
+        # result 2: just strip comments
+        result2 = ["\n", "line 3 \\\n", "  continues on next line\n"]
+
+        # result 3: just strip blank lines
+        result3 = [
+            "# test file\n",
+            "line 3 \\\n",
+            "# intervening comment\n",
+            "  continues on next line\n",
+        ]
+
+        # result 4: default, strip comments, blank lines,
+        # and trailing whitespace
+        result4 = ["line 3 \\", "  continues on next line"]
+
+        # result 5: strip comments and blanks, plus join lines (but don't
+        # "collapse" joined lines
+        result5 = ["line 3   continues on next line"]
+
+        # result 6: strip comments and blanks, plus join lines (and
+        # "collapse" joined lines
+        result6 = ["line 3 continues on next line"]
+
+        def test_input(count, description, file, expected_result):
+            result = file.readlines()
+            assert result == expected_result
+
+        tmp_path = path.Path(self.mkdtemp())
+        filename = tmp_path / 'test.txt'
+        jaraco.path.build({filename.name: TEST_DATA}, tmp_path)
+
+        in_file = TextFile(
+            filename,
+            strip_comments=False,
+            skip_blanks=False,
+            lstrip_ws=False,
+            rstrip_ws=False,
+        )
+        try:
+            test_input(1, "no processing", in_file, result1)
+        finally:
+            in_file.close()
+
+        in_file = TextFile(
+            filename,
+            strip_comments=True,
+            skip_blanks=False,
+            lstrip_ws=False,
+            rstrip_ws=False,
+        )
+        try:
+            test_input(2, "strip comments", in_file, result2)
+        finally:
+            in_file.close()
+
+        in_file = TextFile(
+            filename,
+            strip_comments=False,
+            skip_blanks=True,
+            lstrip_ws=False,
+            rstrip_ws=False,
+        )
+        try:
+            test_input(3, "strip blanks", in_file, result3)
+        finally:
+            in_file.close()
+
+        in_file = TextFile(filename)
+        try:
+            test_input(4, "default processing", in_file, result4)
+        finally:
+            in_file.close()
+
+        in_file = TextFile(
+            filename,
+            strip_comments=True,
+            skip_blanks=True,
+            join_lines=True,
+            rstrip_ws=True,
+        )
+        try:
+            test_input(5, "join lines without collapsing", in_file, result5)
+        finally:
+            in_file.close()
+
+        in_file = TextFile(
+            filename,
+            strip_comments=True,
+            skip_blanks=True,
+            join_lines=True,
+            rstrip_ws=True,
+            collapse_join=True,
+        )
+        try:
+            test_input(6, "join lines with collapsing", in_file, result6)
+        finally:
+            in_file.close()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_util.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_util.py
new file mode 100644
index 00000000..00c9743e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_util.py
@@ -0,0 +1,243 @@
+"""Tests for distutils.util."""
+
+import email
+import email.generator
+import email.policy
+import io
+import os
+import pathlib
+import sys
+import sysconfig as stdlib_sysconfig
+import unittest.mock as mock
+from copy import copy
+from distutils import sysconfig, util
+from distutils.errors import DistutilsByteCompileError, DistutilsPlatformError
+from distutils.util import (
+    byte_compile,
+    change_root,
+    check_environ,
+    convert_path,
+    get_host_platform,
+    get_platform,
+    grok_environment_error,
+    rfc822_escape,
+    split_quoted,
+    strtobool,
+)
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def environment(monkeypatch):
+    monkeypatch.setattr(os, 'name', os.name)
+    monkeypatch.setattr(sys, 'platform', sys.platform)
+    monkeypatch.setattr(sys, 'version', sys.version)
+    monkeypatch.setattr(os, 'sep', os.sep)
+    monkeypatch.setattr(os.path, 'join', os.path.join)
+    monkeypatch.setattr(os.path, 'isabs', os.path.isabs)
+    monkeypatch.setattr(os.path, 'splitdrive', os.path.splitdrive)
+    monkeypatch.setattr(sysconfig, '_config_vars', copy(sysconfig._config_vars))
+
+
+@pytest.mark.usefixtures('save_env')
+class TestUtil:
+    def test_get_host_platform(self):
+        with mock.patch('os.name', 'nt'):
+            with mock.patch('sys.version', '... [... (ARM64)]'):
+                assert get_host_platform() == 'win-arm64'
+            with mock.patch('sys.version', '... [... (ARM)]'):
+                assert get_host_platform() == 'win-arm32'
+
+        with mock.patch('sys.version_info', (3, 9, 0, 'final', 0)):
+            assert get_host_platform() == stdlib_sysconfig.get_platform()
+
+    def test_get_platform(self):
+        with mock.patch('os.name', 'nt'):
+            with mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x86'}):
+                assert get_platform() == 'win32'
+            with mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x64'}):
+                assert get_platform() == 'win-amd64'
+            with mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm'}):
+                assert get_platform() == 'win-arm32'
+            with mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm64'}):
+                assert get_platform() == 'win-arm64'
+
+    def test_convert_path(self):
+        expected = os.sep.join(('', 'home', 'to', 'my', 'stuff'))
+        assert convert_path('/home/to/my/stuff') == expected
+        assert convert_path(pathlib.Path('/home/to/my/stuff')) == expected
+        assert convert_path('.') == os.curdir
+
+    def test_change_root(self):
+        # linux/mac
+        os.name = 'posix'
+
+        def _isabs(path):
+            return path[0] == '/'
+
+        os.path.isabs = _isabs
+
+        def _join(*path):
+            return '/'.join(path)
+
+        os.path.join = _join
+
+        assert change_root('/root', '/old/its/here') == '/root/old/its/here'
+        assert change_root('/root', 'its/here') == '/root/its/here'
+
+        # windows
+        os.name = 'nt'
+        os.sep = '\\'
+
+        def _isabs(path):
+            return path.startswith('c:\\')
+
+        os.path.isabs = _isabs
+
+        def _splitdrive(path):
+            if path.startswith('c:'):
+                return ('', path.replace('c:', ''))
+            return ('', path)
+
+        os.path.splitdrive = _splitdrive
+
+        def _join(*path):
+            return '\\'.join(path)
+
+        os.path.join = _join
+
+        assert (
+            change_root('c:\\root', 'c:\\old\\its\\here') == 'c:\\root\\old\\its\\here'
+        )
+        assert change_root('c:\\root', 'its\\here') == 'c:\\root\\its\\here'
+
+        # BugsBunny os (it's a great os)
+        os.name = 'BugsBunny'
+        with pytest.raises(DistutilsPlatformError):
+            change_root('c:\\root', 'its\\here')
+
+        # XXX platforms to be covered: mac
+
+    def test_check_environ(self):
+        util.check_environ.cache_clear()
+        os.environ.pop('HOME', None)
+
+        check_environ()
+
+        assert os.environ['PLAT'] == get_platform()
+
+    @pytest.mark.skipif("os.name != 'posix'")
+    def test_check_environ_getpwuid(self):
+        util.check_environ.cache_clear()
+        os.environ.pop('HOME', None)
+
+        import pwd
+
+        # only set pw_dir field, other fields are not used
+        result = pwd.struct_passwd((
+            None,
+            None,
+            None,
+            None,
+            None,
+            '/home/distutils',
+            None,
+        ))
+        with mock.patch.object(pwd, 'getpwuid', return_value=result):
+            check_environ()
+            assert os.environ['HOME'] == '/home/distutils'
+
+        util.check_environ.cache_clear()
+        os.environ.pop('HOME', None)
+
+        # bpo-10496: Catch pwd.getpwuid() error
+        with mock.patch.object(pwd, 'getpwuid', side_effect=KeyError):
+            check_environ()
+            assert 'HOME' not in os.environ
+
+    def test_split_quoted(self):
+        assert split_quoted('""one"" "two" \'three\' \\four') == [
+            'one',
+            'two',
+            'three',
+            'four',
+        ]
+
+    def test_strtobool(self):
+        yes = ('y', 'Y', 'yes', 'True', 't', 'true', 'True', 'On', 'on', '1')
+        no = ('n', 'no', 'f', 'false', 'off', '0', 'Off', 'No', 'N')
+
+        for y in yes:
+            assert strtobool(y)
+
+        for n in no:
+            assert not strtobool(n)
+
+    indent = 8 * ' '
+
+    @pytest.mark.parametrize(
+        "given,wanted",
+        [
+            # 0x0b, 0x0c, ..., etc are also considered a line break by Python
+            ("hello\x0b\nworld\n", f"hello\x0b{indent}\n{indent}world\n{indent}"),
+            ("hello\x1eworld", f"hello\x1e{indent}world"),
+            ("", ""),
+            (
+                "I am a\npoor\nlonesome\nheader\n",
+                f"I am a\n{indent}poor\n{indent}lonesome\n{indent}header\n{indent}",
+            ),
+        ],
+    )
+    def test_rfc822_escape(self, given, wanted):
+        """
+        We want to ensure a multi-line header parses correctly.
+
+        For interoperability, the escaped value should also "round-trip" over
+        `email.generator.Generator.flatten` and `email.message_from_*`
+        (see pypa/setuptools#4033).
+
+        The main issue is that internally `email.policy.EmailPolicy` uses
+        `splitlines` which will split on some control chars. If all the new lines
+        are not prefixed with spaces, the parser will interrupt reading
+        the current header and produce an incomplete value, while
+        incorrectly interpreting the rest of the headers as part of the payload.
+        """
+        res = rfc822_escape(given)
+
+        policy = email.policy.EmailPolicy(
+            utf8=True,
+            mangle_from_=False,
+            max_line_length=0,
+        )
+        with io.StringIO() as buffer:
+            raw = f"header: {res}\nother-header: 42\n\npayload\n"
+            orig = email.message_from_string(raw)
+            email.generator.Generator(buffer, policy=policy).flatten(orig)
+            buffer.seek(0)
+            regen = email.message_from_file(buffer)
+
+        for msg in (orig, regen):
+            assert msg.get_payload() == "payload\n"
+            assert msg["other-header"] == "42"
+            # Generator may replace control chars with `\n`
+            assert set(msg["header"].splitlines()) == set(res.splitlines())
+
+        assert res == wanted
+
+    def test_dont_write_bytecode(self):
+        # makes sure byte_compile raise a DistutilsError
+        # if sys.dont_write_bytecode is True
+        old_dont_write_bytecode = sys.dont_write_bytecode
+        sys.dont_write_bytecode = True
+        try:
+            with pytest.raises(DistutilsByteCompileError):
+                byte_compile([])
+        finally:
+            sys.dont_write_bytecode = old_dont_write_bytecode
+
+    def test_grok_environment_error(self):
+        # test obsolete function to ensure backward compat (#4931)
+        exc = OSError("Unable to find batch file")
+        msg = grok_environment_error(exc)
+        assert msg == "error: Unable to find batch file"
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_version.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_version.py
new file mode 100644
index 00000000..b68f0977
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_version.py
@@ -0,0 +1,80 @@
+"""Tests for distutils.version."""
+
+import distutils
+from distutils.version import LooseVersion, StrictVersion
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def suppress_deprecation():
+    with distutils.version.suppress_known_deprecation():
+        yield
+
+
+class TestVersion:
+    def test_prerelease(self):
+        version = StrictVersion('1.2.3a1')
+        assert version.version == (1, 2, 3)
+        assert version.prerelease == ('a', 1)
+        assert str(version) == '1.2.3a1'
+
+        version = StrictVersion('1.2.0')
+        assert str(version) == '1.2'
+
+    def test_cmp_strict(self):
+        versions = (
+            ('1.5.1', '1.5.2b2', -1),
+            ('161', '3.10a', ValueError),
+            ('8.02', '8.02', 0),
+            ('3.4j', '1996.07.12', ValueError),
+            ('3.2.pl0', '3.1.1.6', ValueError),
+            ('2g6', '11g', ValueError),
+            ('0.9', '2.2', -1),
+            ('1.2.1', '1.2', 1),
+            ('1.1', '1.2.2', -1),
+            ('1.2', '1.1', 1),
+            ('1.2.1', '1.2.2', -1),
+            ('1.2.2', '1.2', 1),
+            ('1.2', '1.2.2', -1),
+            ('0.4.0', '0.4', 0),
+            ('1.13++', '5.5.kw', ValueError),
+        )
+
+        for v1, v2, wanted in versions:
+            try:
+                res = StrictVersion(v1)._cmp(StrictVersion(v2))
+            except ValueError:
+                if wanted is ValueError:
+                    continue
+                else:
+                    raise AssertionError(f"cmp({v1}, {v2}) shouldn't raise ValueError")
+            assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}'
+            res = StrictVersion(v1)._cmp(v2)
+            assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}'
+            res = StrictVersion(v1)._cmp(object())
+            assert res is NotImplemented, (
+                f'cmp({v1}, {v2}) should be NotImplemented, got {res}'
+            )
+
+    def test_cmp(self):
+        versions = (
+            ('1.5.1', '1.5.2b2', -1),
+            ('161', '3.10a', 1),
+            ('8.02', '8.02', 0),
+            ('3.4j', '1996.07.12', -1),
+            ('3.2.pl0', '3.1.1.6', 1),
+            ('2g6', '11g', -1),
+            ('0.960923', '2.2beta29', -1),
+            ('1.13++', '5.5.kw', -1),
+        )
+
+        for v1, v2, wanted in versions:
+            res = LooseVersion(v1)._cmp(LooseVersion(v2))
+            assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}'
+            res = LooseVersion(v1)._cmp(v2)
+            assert res == wanted, f'cmp({v1}, {v2}) should be {wanted}, got {res}'
+            res = LooseVersion(v1)._cmp(object())
+            assert res is NotImplemented, (
+                f'cmp({v1}, {v2}) should be NotImplemented, got {res}'
+            )
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_versionpredicate.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_versionpredicate.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/test_versionpredicate.py
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/unix_compat.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/unix_compat.py
new file mode 100644
index 00000000..a5d9ee45
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/tests/unix_compat.py
@@ -0,0 +1,17 @@
+import sys
+
+try:
+    import grp
+    import pwd
+except ImportError:
+    grp = pwd = None
+
+import pytest
+
+UNIX_ID_SUPPORT = grp and pwd
+UID_0_SUPPORT = UNIX_ID_SUPPORT and sys.platform != "cygwin"
+
+require_unix_id = pytest.mark.skipif(
+    not UNIX_ID_SUPPORT, reason="Requires grp and pwd support"
+)
+require_uid_0 = pytest.mark.skipif(not UID_0_SUPPORT, reason="Requires UID 0 support")
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/text_file.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/text_file.py
new file mode 100644
index 00000000..89d9048d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/text_file.py
@@ -0,0 +1,286 @@
+"""text_file
+
+provides the TextFile class, which gives an interface to text files
+that (optionally) takes care of stripping comments, ignoring blank
+lines, and joining lines with backslashes."""
+
+import sys
+
+
+class TextFile:
+    """Provides a file-like object that takes care of all the things you
+    commonly want to do when processing a text file that has some
+    line-by-line syntax: strip comments (as long as "#" is your
+    comment character), skip blank lines, join adjacent lines by
+    escaping the newline (ie. backslash at end of line), strip
+    leading and/or trailing whitespace.  All of these are optional
+    and independently controllable.
+
+    Provides a 'warn()' method so you can generate warning messages that
+    report physical line number, even if the logical line in question
+    spans multiple physical lines.  Also provides 'unreadline()' for
+    implementing line-at-a-time lookahead.
+
+    Constructor is called as:
+
+        TextFile (filename=None, file=None, **options)
+
+    It bombs (RuntimeError) if both 'filename' and 'file' are None;
+    'filename' should be a string, and 'file' a file object (or
+    something that provides 'readline()' and 'close()' methods).  It is
+    recommended that you supply at least 'filename', so that TextFile
+    can include it in warning messages.  If 'file' is not supplied,
+    TextFile creates its own using 'io.open()'.
+
+    The options are all boolean, and affect the value returned by
+    'readline()':
+      strip_comments [default: true]
+        strip from "#" to end-of-line, as well as any whitespace
+        leading up to the "#" -- unless it is escaped by a backslash
+      lstrip_ws [default: false]
+        strip leading whitespace from each line before returning it
+      rstrip_ws [default: true]
+        strip trailing whitespace (including line terminator!) from
+        each line before returning it
+      skip_blanks [default: true}
+        skip lines that are empty *after* stripping comments and
+        whitespace.  (If both lstrip_ws and rstrip_ws are false,
+        then some lines may consist of solely whitespace: these will
+        *not* be skipped, even if 'skip_blanks' is true.)
+      join_lines [default: false]
+        if a backslash is the last non-newline character on a line
+        after stripping comments and whitespace, join the following line
+        to it to form one "logical line"; if N consecutive lines end
+        with a backslash, then N+1 physical lines will be joined to
+        form one logical line.
+      collapse_join [default: false]
+        strip leading whitespace from lines that are joined to their
+        predecessor; only matters if (join_lines and not lstrip_ws)
+      errors [default: 'strict']
+        error handler used to decode the file content
+
+    Note that since 'rstrip_ws' can strip the trailing newline, the
+    semantics of 'readline()' must differ from those of the builtin file
+    object's 'readline()' method!  In particular, 'readline()' returns
+    None for end-of-file: an empty string might just be a blank line (or
+    an all-whitespace line), if 'rstrip_ws' is true but 'skip_blanks' is
+    not."""
+
+    default_options = {
+        'strip_comments': 1,
+        'skip_blanks': 1,
+        'lstrip_ws': 0,
+        'rstrip_ws': 1,
+        'join_lines': 0,
+        'collapse_join': 0,
+        'errors': 'strict',
+    }
+
+    def __init__(self, filename=None, file=None, **options):
+        """Construct a new TextFile object.  At least one of 'filename'
+        (a string) and 'file' (a file-like object) must be supplied.
+        They keyword argument options are described above and affect
+        the values returned by 'readline()'."""
+        if filename is None and file is None:
+            raise RuntimeError(
+                "you must supply either or both of 'filename' and 'file'"
+            )
+
+        # set values for all options -- either from client option hash
+        # or fallback to default_options
+        for opt in self.default_options.keys():
+            if opt in options:
+                setattr(self, opt, options[opt])
+            else:
+                setattr(self, opt, self.default_options[opt])
+
+        # sanity check client option hash
+        for opt in options.keys():
+            if opt not in self.default_options:
+                raise KeyError(f"invalid TextFile option '{opt}'")
+
+        if file is None:
+            self.open(filename)
+        else:
+            self.filename = filename
+            self.file = file
+            self.current_line = 0  # assuming that file is at BOF!
+
+        # 'linebuf' is a stack of lines that will be emptied before we
+        # actually read from the file; it's only populated by an
+        # 'unreadline()' operation
+        self.linebuf = []
+
+    def open(self, filename):
+        """Open a new file named 'filename'.  This overrides both the
+        'filename' and 'file' arguments to the constructor."""
+        self.filename = filename
+        self.file = open(self.filename, errors=self.errors, encoding='utf-8')
+        self.current_line = 0
+
+    def close(self):
+        """Close the current file and forget everything we know about it
+        (filename, current line number)."""
+        file = self.file
+        self.file = None
+        self.filename = None
+        self.current_line = None
+        file.close()
+
+    def gen_error(self, msg, line=None):
+        outmsg = []
+        if line is None:
+            line = self.current_line
+        outmsg.append(self.filename + ", ")
+        if isinstance(line, (list, tuple)):
+            outmsg.append("lines {}-{}: ".format(*line))
+        else:
+            outmsg.append(f"line {int(line)}: ")
+        outmsg.append(str(msg))
+        return "".join(outmsg)
+
+    def error(self, msg, line=None):
+        raise ValueError("error: " + self.gen_error(msg, line))
+
+    def warn(self, msg, line=None):
+        """Print (to stderr) a warning message tied to the current logical
+        line in the current file.  If the current logical line in the
+        file spans multiple physical lines, the warning refers to the
+        whole range, eg. "lines 3-5".  If 'line' supplied, it overrides
+        the current line number; it may be a list or tuple to indicate a
+        range of physical lines, or an integer for a single physical
+        line."""
+        sys.stderr.write("warning: " + self.gen_error(msg, line) + "\n")
+
+    def readline(self):  # noqa: C901
+        """Read and return a single logical line from the current file (or
+        from an internal buffer if lines have previously been "unread"
+        with 'unreadline()').  If the 'join_lines' option is true, this
+        may involve reading multiple physical lines concatenated into a
+        single string.  Updates the current line number, so calling
+        'warn()' after 'readline()' emits a warning about the physical
+        line(s) just read.  Returns None on end-of-file, since the empty
+        string can occur if 'rstrip_ws' is true but 'strip_blanks' is
+        not."""
+        # If any "unread" lines waiting in 'linebuf', return the top
+        # one.  (We don't actually buffer read-ahead data -- lines only
+        # get put in 'linebuf' if the client explicitly does an
+        # 'unreadline()'.
+        if self.linebuf:
+            line = self.linebuf[-1]
+            del self.linebuf[-1]
+            return line
+
+        buildup_line = ''
+
+        while True:
+            # read the line, make it None if EOF
+            line = self.file.readline()
+            if line == '':
+                line = None
+
+            if self.strip_comments and line:
+                # Look for the first "#" in the line.  If none, never
+                # mind.  If we find one and it's the first character, or
+                # is not preceded by "\", then it starts a comment --
+                # strip the comment, strip whitespace before it, and
+                # carry on.  Otherwise, it's just an escaped "#", so
+                # unescape it (and any other escaped "#"'s that might be
+                # lurking in there) and otherwise leave the line alone.
+
+                pos = line.find("#")
+                if pos == -1:  # no "#" -- no comments
+                    pass
+
+                # It's definitely a comment -- either "#" is the first
+                # character, or it's elsewhere and unescaped.
+                elif pos == 0 or line[pos - 1] != "\\":
+                    # Have to preserve the trailing newline, because it's
+                    # the job of a later step (rstrip_ws) to remove it --
+                    # and if rstrip_ws is false, we'd better preserve it!
+                    # (NB. this means that if the final line is all comment
+                    # and has no trailing newline, we will think that it's
+                    # EOF; I think that's OK.)
+                    eol = (line[-1] == '\n') and '\n' or ''
+                    line = line[0:pos] + eol
+
+                    # If all that's left is whitespace, then skip line
+                    # *now*, before we try to join it to 'buildup_line' --
+                    # that way constructs like
+                    #   hello \\
+                    #   # comment that should be ignored
+                    #   there
+                    # result in "hello there".
+                    if line.strip() == "":
+                        continue
+                else:  # it's an escaped "#"
+                    line = line.replace("\\#", "#")
+
+            # did previous line end with a backslash? then accumulate
+            if self.join_lines and buildup_line:
+                # oops: end of file
+                if line is None:
+                    self.warn("continuation line immediately precedes end-of-file")
+                    return buildup_line
+
+                if self.collapse_join:
+                    line = line.lstrip()
+                line = buildup_line + line
+
+                # careful: pay attention to line number when incrementing it
+                if isinstance(self.current_line, list):
+                    self.current_line[1] = self.current_line[1] + 1
+                else:
+                    self.current_line = [self.current_line, self.current_line + 1]
+            # just an ordinary line, read it as usual
+            else:
+                if line is None:  # eof
+                    return None
+
+                # still have to be careful about incrementing the line number!
+                if isinstance(self.current_line, list):
+                    self.current_line = self.current_line[1] + 1
+                else:
+                    self.current_line = self.current_line + 1
+
+            # strip whitespace however the client wants (leading and
+            # trailing, or one or the other, or neither)
+            if self.lstrip_ws and self.rstrip_ws:
+                line = line.strip()
+            elif self.lstrip_ws:
+                line = line.lstrip()
+            elif self.rstrip_ws:
+                line = line.rstrip()
+
+            # blank line (whether we rstrip'ed or not)? skip to next line
+            # if appropriate
+            if line in ('', '\n') and self.skip_blanks:
+                continue
+
+            if self.join_lines:
+                if line[-1] == '\\':
+                    buildup_line = line[:-1]
+                    continue
+
+                if line[-2:] == '\\\n':
+                    buildup_line = line[0:-2] + '\n'
+                    continue
+
+            # well, I guess there's some actual content there: return it
+            return line
+
+    def readlines(self):
+        """Read and return the list of all logical lines remaining in the
+        current file."""
+        lines = []
+        while True:
+            line = self.readline()
+            if line is None:
+                return lines
+            lines.append(line)
+
+    def unreadline(self, line):
+        """Push 'line' (a string) onto an internal buffer that will be
+        checked by future 'readline()' calls.  Handy for implementing
+        a parser with line-at-a-time lookahead."""
+        self.linebuf.append(line)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/unixccompiler.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/unixccompiler.py
new file mode 100644
index 00000000..20b8ce6b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/unixccompiler.py
@@ -0,0 +1,9 @@
+import importlib
+
+from .compilers.C import unix
+
+UnixCCompiler = unix.Compiler
+
+# ensure import of unixccompiler implies ccompiler imported
+# (pypa/setuptools#4871)
+importlib.import_module('distutils.ccompiler')
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/util.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/util.py
new file mode 100644
index 00000000..6dbe049f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/util.py
@@ -0,0 +1,518 @@
+"""distutils.util
+
+Miscellaneous utility functions -- anything that doesn't fit into
+one of the other *util.py modules.
+"""
+
+from __future__ import annotations
+
+import functools
+import importlib.util
+import os
+import pathlib
+import re
+import string
+import subprocess
+import sys
+import sysconfig
+import tempfile
+from collections.abc import Callable, Iterable, Mapping
+from typing import TYPE_CHECKING, AnyStr
+
+from jaraco.functools import pass_none
+
+from ._log import log
+from ._modified import newer
+from .errors import DistutilsByteCompileError, DistutilsPlatformError
+from .spawn import spawn
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeVarTuple, Unpack
+
+    _Ts = TypeVarTuple("_Ts")
+
+
+def get_host_platform() -> str:
+    """
+    Return a string that identifies the current platform. Use this
+    function to distinguish platform-specific build directories and
+    platform-specific built distributions.
+    """
+
+    # This function initially exposed platforms as defined in Python 3.9
+    # even with older Python versions when distutils was split out.
+    # Now it delegates to stdlib sysconfig.
+
+    return sysconfig.get_platform()
+
+
+def get_platform() -> str:
+    if os.name == 'nt':
+        TARGET_TO_PLAT = {
+            'x86': 'win32',
+            'x64': 'win-amd64',
+            'arm': 'win-arm32',
+            'arm64': 'win-arm64',
+        }
+        target = os.environ.get('VSCMD_ARG_TGT_ARCH')
+        return TARGET_TO_PLAT.get(target) or get_host_platform()
+    return get_host_platform()
+
+
+if sys.platform == 'darwin':
+    _syscfg_macosx_ver = None  # cache the version pulled from sysconfig
+MACOSX_VERSION_VAR = 'MACOSX_DEPLOYMENT_TARGET'
+
+
+def _clear_cached_macosx_ver():
+    """For testing only. Do not call."""
+    global _syscfg_macosx_ver
+    _syscfg_macosx_ver = None
+
+
+def get_macosx_target_ver_from_syscfg():
+    """Get the version of macOS latched in the Python interpreter configuration.
+    Returns the version as a string or None if can't obtain one. Cached."""
+    global _syscfg_macosx_ver
+    if _syscfg_macosx_ver is None:
+        from distutils import sysconfig
+
+        ver = sysconfig.get_config_var(MACOSX_VERSION_VAR) or ''
+        if ver:
+            _syscfg_macosx_ver = ver
+    return _syscfg_macosx_ver
+
+
+def get_macosx_target_ver():
+    """Return the version of macOS for which we are building.
+
+    The target version defaults to the version in sysconfig latched at time
+    the Python interpreter was built, unless overridden by an environment
+    variable. If neither source has a value, then None is returned"""
+
+    syscfg_ver = get_macosx_target_ver_from_syscfg()
+    env_ver = os.environ.get(MACOSX_VERSION_VAR)
+
+    if env_ver:
+        # Validate overridden version against sysconfig version, if have both.
+        # Ensure that the deployment target of the build process is not less
+        # than 10.3 if the interpreter was built for 10.3 or later.  This
+        # ensures extension modules are built with correct compatibility
+        # values, specifically LDSHARED which can use
+        # '-undefined dynamic_lookup' which only works on >= 10.3.
+        if (
+            syscfg_ver
+            and split_version(syscfg_ver) >= [10, 3]
+            and split_version(env_ver) < [10, 3]
+        ):
+            my_msg = (
+                '$' + MACOSX_VERSION_VAR + ' mismatch: '
+                f'now "{env_ver}" but "{syscfg_ver}" during configure; '
+                'must use 10.3 or later'
+            )
+            raise DistutilsPlatformError(my_msg)
+        return env_ver
+    return syscfg_ver
+
+
+def split_version(s: str) -> list[int]:
+    """Convert a dot-separated string into a list of numbers for comparisons"""
+    return [int(n) for n in s.split('.')]
+
+
+@pass_none
+def convert_path(pathname: str | os.PathLike[str]) -> str:
+    r"""
+    Allow for pathlib.Path inputs, coax to a native path string.
+
+    If None is passed, will just pass it through as
+    Setuptools relies on this behavior.
+
+    >>> convert_path(None) is None
+    True
+
+    Removes empty paths.
+
+    >>> convert_path('foo/./bar').replace('\\', '/')
+    'foo/bar'
+    """
+    return os.fspath(pathlib.PurePath(pathname))
+
+
+def change_root(
+    new_root: AnyStr | os.PathLike[AnyStr], pathname: AnyStr | os.PathLike[AnyStr]
+) -> AnyStr:
+    """Return 'pathname' with 'new_root' prepended.  If 'pathname' is
+    relative, this is equivalent to "os.path.join(new_root,pathname)".
+    Otherwise, it requires making 'pathname' relative and then joining the
+    two, which is tricky on DOS/Windows and Mac OS.
+    """
+    if os.name == 'posix':
+        if not os.path.isabs(pathname):
+            return os.path.join(new_root, pathname)
+        else:
+            return os.path.join(new_root, pathname[1:])
+
+    elif os.name == 'nt':
+        (drive, path) = os.path.splitdrive(pathname)
+        if path[0] == os.sep:
+            path = path[1:]
+        return os.path.join(new_root, path)
+
+    raise DistutilsPlatformError(f"nothing known about platform '{os.name}'")
+
+
+@functools.lru_cache
+def check_environ() -> None:
+    """Ensure that 'os.environ' has all the environment variables we
+    guarantee that users can use in config files, command-line options,
+    etc.  Currently this includes:
+      HOME - user's home directory (Unix only)
+      PLAT - description of the current platform, including hardware
+             and OS (see 'get_platform()')
+    """
+    if os.name == 'posix' and 'HOME' not in os.environ:
+        try:
+            import pwd
+
+            os.environ['HOME'] = pwd.getpwuid(os.getuid())[5]
+        except (ImportError, KeyError):
+            # bpo-10496: if the current user identifier doesn't exist in the
+            # password database, do nothing
+            pass
+
+    if 'PLAT' not in os.environ:
+        os.environ['PLAT'] = get_platform()
+
+
+def subst_vars(s, local_vars: Mapping[str, object]) -> str:
+    """
+    Perform variable substitution on 'string'.
+    Variables are indicated by format-style braces ("{var}").
+    Variable is substituted by the value found in the 'local_vars'
+    dictionary or in 'os.environ' if it's not in 'local_vars'.
+    'os.environ' is first checked/augmented to guarantee that it contains
+    certain values: see 'check_environ()'.  Raise ValueError for any
+    variables not found in either 'local_vars' or 'os.environ'.
+    """
+    check_environ()
+    lookup = dict(os.environ)
+    lookup.update((name, str(value)) for name, value in local_vars.items())
+    try:
+        return _subst_compat(s).format_map(lookup)
+    except KeyError as var:
+        raise ValueError(f"invalid variable {var}")
+
+
+def _subst_compat(s):
+    """
+    Replace shell/Perl-style variable substitution with
+    format-style. For compatibility.
+    """
+
+    def _subst(match):
+        return f'{{{match.group(1)}}}'
+
+    repl = re.sub(r'\$([a-zA-Z_][a-zA-Z_0-9]*)', _subst, s)
+    if repl != s:
+        import warnings
+
+        warnings.warn(
+            "shell/Perl-style substitutions are deprecated",
+            DeprecationWarning,
+        )
+    return repl
+
+
+def grok_environment_error(exc: object, prefix: str = "error: ") -> str:
+    # Function kept for backward compatibility.
+    # Used to try clever things with EnvironmentErrors,
+    # but nowadays str(exception) produces good messages.
+    return prefix + str(exc)
+
+
+# Needed by 'split_quoted()'
+_wordchars_re = _squote_re = _dquote_re = None
+
+
+def _init_regex():
+    global _wordchars_re, _squote_re, _dquote_re
+    _wordchars_re = re.compile(rf'[^\\\'\"{string.whitespace} ]*')
+    _squote_re = re.compile(r"'(?:[^'\\]|\\.)*'")
+    _dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"')
+
+
+def split_quoted(s: str) -> list[str]:
+    """Split a string up according to Unix shell-like rules for quotes and
+    backslashes.  In short: words are delimited by spaces, as long as those
+    spaces are not escaped by a backslash, or inside a quoted string.
+    Single and double quotes are equivalent, and the quote characters can
+    be backslash-escaped.  The backslash is stripped from any two-character
+    escape sequence, leaving only the escaped character.  The quote
+    characters are stripped from any quoted string.  Returns a list of
+    words.
+    """
+
+    # This is a nice algorithm for splitting up a single string, since it
+    # doesn't require character-by-character examination.  It was a little
+    # bit of a brain-bender to get it working right, though...
+    if _wordchars_re is None:
+        _init_regex()
+
+    s = s.strip()
+    words = []
+    pos = 0
+
+    while s:
+        m = _wordchars_re.match(s, pos)
+        end = m.end()
+        if end == len(s):
+            words.append(s[:end])
+            break
+
+        if s[end] in string.whitespace:
+            # unescaped, unquoted whitespace: now
+            # we definitely have a word delimiter
+            words.append(s[:end])
+            s = s[end:].lstrip()
+            pos = 0
+
+        elif s[end] == '\\':
+            # preserve whatever is being escaped;
+            # will become part of the current word
+            s = s[:end] + s[end + 1 :]
+            pos = end + 1
+
+        else:
+            if s[end] == "'":  # slurp singly-quoted string
+                m = _squote_re.match(s, end)
+            elif s[end] == '"':  # slurp doubly-quoted string
+                m = _dquote_re.match(s, end)
+            else:
+                raise RuntimeError(f"this can't happen (bad char '{s[end]}')")
+
+            if m is None:
+                raise ValueError(f"bad string (mismatched {s[end]} quotes?)")
+
+            (beg, end) = m.span()
+            s = s[:beg] + s[beg + 1 : end - 1] + s[end:]
+            pos = m.end() - 2
+
+        if pos >= len(s):
+            words.append(s)
+            break
+
+    return words
+
+
+# split_quoted ()
+
+
+def execute(
+    func: Callable[[Unpack[_Ts]], object],
+    args: tuple[Unpack[_Ts]],
+    msg: object = None,
+    verbose: bool = False,
+    dry_run: bool = False,
+) -> None:
+    """
+    Perform some action that affects the outside world (e.g. by
+    writing to the filesystem). Such actions are special because they
+    are disabled by the 'dry_run' flag. This method handles that
+    complication; simply supply the
+    function to call and an argument tuple for it (to embody the
+    "external action" being performed) and an optional message to
+    emit.
+    """
+    if msg is None:
+        msg = f"{func.__name__}{args!r}"
+        if msg[-2:] == ',)':  # correct for singleton tuple
+            msg = msg[0:-2] + ')'
+
+    log.info(msg)
+    if not dry_run:
+        func(*args)
+
+
+def strtobool(val: str) -> bool:
+    """Convert a string representation of truth to true (1) or false (0).
+
+    True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
+    are 'n', 'no', 'f', 'false', 'off', and '0'.  Raises ValueError if
+    'val' is anything else.
+    """
+    val = val.lower()
+    if val in ('y', 'yes', 't', 'true', 'on', '1'):
+        return True
+    elif val in ('n', 'no', 'f', 'false', 'off', '0'):
+        return False
+    else:
+        raise ValueError(f"invalid truth value {val!r}")
+
+
+def byte_compile(  # noqa: C901
+    py_files: Iterable[str],
+    optimize: int = 0,
+    force: bool = False,
+    prefix: str | None = None,
+    base_dir: str | None = None,
+    verbose: bool = True,
+    dry_run: bool = False,
+    direct: bool | None = None,
+) -> None:
+    """Byte-compile a collection of Python source files to .pyc
+    files in a __pycache__ subdirectory.  'py_files' is a list
+    of files to compile; any files that don't end in ".py" are silently
+    skipped.  'optimize' must be one of the following:
+      0 - don't optimize
+      1 - normal optimization (like "python -O")
+      2 - extra optimization (like "python -OO")
+    If 'force' is true, all files are recompiled regardless of
+    timestamps.
+
+    The source filename encoded in each bytecode file defaults to the
+    filenames listed in 'py_files'; you can modify these with 'prefix' and
+    'basedir'.  'prefix' is a string that will be stripped off of each
+    source filename, and 'base_dir' is a directory name that will be
+    prepended (after 'prefix' is stripped).  You can supply either or both
+    (or neither) of 'prefix' and 'base_dir', as you wish.
+
+    If 'dry_run' is true, doesn't actually do anything that would
+    affect the filesystem.
+
+    Byte-compilation is either done directly in this interpreter process
+    with the standard py_compile module, or indirectly by writing a
+    temporary script and executing it.  Normally, you should let
+    'byte_compile()' figure out to use direct compilation or not (see
+    the source for details).  The 'direct' flag is used by the script
+    generated in indirect mode; unless you know what you're doing, leave
+    it set to None.
+    """
+
+    # nothing is done if sys.dont_write_bytecode is True
+    if sys.dont_write_bytecode:
+        raise DistutilsByteCompileError('byte-compiling is disabled.')
+
+    # First, if the caller didn't force us into direct or indirect mode,
+    # figure out which mode we should be in.  We take a conservative
+    # approach: choose direct mode *only* if the current interpreter is
+    # in debug mode and optimize is 0.  If we're not in debug mode (-O
+    # or -OO), we don't know which level of optimization this
+    # interpreter is running with, so we can't do direct
+    # byte-compilation and be certain that it's the right thing.  Thus,
+    # always compile indirectly if the current interpreter is in either
+    # optimize mode, or if either optimization level was requested by
+    # the caller.
+    if direct is None:
+        direct = __debug__ and optimize == 0
+
+    # "Indirect" byte-compilation: write a temporary script and then
+    # run it with the appropriate flags.
+    if not direct:
+        (script_fd, script_name) = tempfile.mkstemp(".py")
+        log.info("writing byte-compilation script '%s'", script_name)
+        if not dry_run:
+            script = os.fdopen(script_fd, "w", encoding='utf-8')
+
+            with script:
+                script.write(
+                    """\
+from distutils.util import byte_compile
+files = [
+"""
+                )
+
+                # XXX would be nice to write absolute filenames, just for
+                # safety's sake (script should be more robust in the face of
+                # chdir'ing before running it).  But this requires abspath'ing
+                # 'prefix' as well, and that breaks the hack in build_lib's
+                # 'byte_compile()' method that carefully tacks on a trailing
+                # slash (os.sep really) to make sure the prefix here is "just
+                # right".  This whole prefix business is rather delicate -- the
+                # problem is that it's really a directory, but I'm treating it
+                # as a dumb string, so trailing slashes and so forth matter.
+
+                script.write(",\n".join(map(repr, py_files)) + "]\n")
+                script.write(
+                    f"""
+byte_compile(files, optimize={optimize!r}, force={force!r},
+             prefix={prefix!r}, base_dir={base_dir!r},
+             verbose={verbose!r}, dry_run=False,
+             direct=True)
+"""
+                )
+
+        cmd = [sys.executable]
+        cmd.extend(subprocess._optim_args_from_interpreter_flags())
+        cmd.append(script_name)
+        spawn(cmd, dry_run=dry_run)
+        execute(os.remove, (script_name,), f"removing {script_name}", dry_run=dry_run)
+
+    # "Direct" byte-compilation: use the py_compile module to compile
+    # right here, right now.  Note that the script generated in indirect
+    # mode simply calls 'byte_compile()' in direct mode, a weird sort of
+    # cross-process recursion.  Hey, it works!
+    else:
+        from py_compile import compile
+
+        for file in py_files:
+            if file[-3:] != ".py":
+                # This lets us be lazy and not filter filenames in
+                # the "install_lib" command.
+                continue
+
+            # Terminology from the py_compile module:
+            #   cfile - byte-compiled file
+            #   dfile - purported source filename (same as 'file' by default)
+            if optimize >= 0:
+                opt = '' if optimize == 0 else optimize
+                cfile = importlib.util.cache_from_source(file, optimization=opt)
+            else:
+                cfile = importlib.util.cache_from_source(file)
+            dfile = file
+            if prefix:
+                if file[: len(prefix)] != prefix:
+                    raise ValueError(
+                        f"invalid prefix: filename {file!r} doesn't start with {prefix!r}"
+                    )
+                dfile = dfile[len(prefix) :]
+            if base_dir:
+                dfile = os.path.join(base_dir, dfile)
+
+            cfile_base = os.path.basename(cfile)
+            if direct:
+                if force or newer(file, cfile):
+                    log.info("byte-compiling %s to %s", file, cfile_base)
+                    if not dry_run:
+                        compile(file, cfile, dfile)
+                else:
+                    log.debug("skipping byte-compilation of %s to %s", file, cfile_base)
+
+
+def rfc822_escape(header: str) -> str:
+    """Return a version of the string escaped for inclusion in an
+    RFC-822 header, by ensuring there are 8 spaces space after each newline.
+    """
+    indent = 8 * " "
+    lines = header.splitlines(keepends=True)
+
+    # Emulate the behaviour of `str.split`
+    # (the terminal line break in `splitlines` does not result in an extra line):
+    ends_in_newline = lines and lines[-1].splitlines()[0] != lines[-1]
+    suffix = indent if ends_in_newline else ""
+
+    return indent.join(lines) + suffix
+
+
+def is_mingw() -> bool:
+    """Returns True if the current platform is mingw.
+
+    Python compiled with Mingw-w64 has sys.platform == 'win32' and
+    get_platform() starts with 'mingw'.
+    """
+    return sys.platform == 'win32' and get_platform().startswith('mingw')
+
+
+def is_freethreaded():
+    """Return True if the Python interpreter is built with free threading support."""
+    return bool(sysconfig.get_config_var('Py_GIL_DISABLED'))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/version.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/version.py
new file mode 100644
index 00000000..2223ee9c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/version.py
@@ -0,0 +1,348 @@
+#
+# distutils/version.py
+#
+# Implements multiple version numbering conventions for the
+# Python Module Distribution Utilities.
+#
+# $Id$
+#
+
+"""Provides classes to represent module version numbers (one class for
+each style of version numbering).  There are currently two such classes
+implemented: StrictVersion and LooseVersion.
+
+Every version number class implements the following interface:
+  * the 'parse' method takes a string and parses it to some internal
+    representation; if the string is an invalid version number,
+    'parse' raises a ValueError exception
+  * the class constructor takes an optional string argument which,
+    if supplied, is passed to 'parse'
+  * __str__ reconstructs the string that was passed to 'parse' (or
+    an equivalent string -- ie. one that will generate an equivalent
+    version number instance)
+  * __repr__ generates Python code to recreate the version number instance
+  * _cmp compares the current instance with either another instance
+    of the same class or a string (which will be parsed to an instance
+    of the same class, thus must follow the same rules)
+"""
+
+import contextlib
+import re
+import warnings
+
+
+@contextlib.contextmanager
+def suppress_known_deprecation():
+    with warnings.catch_warnings(record=True) as ctx:
+        warnings.filterwarnings(
+            action='default',
+            category=DeprecationWarning,
+            message="distutils Version classes are deprecated.",
+        )
+        yield ctx
+
+
+class Version:
+    """Abstract base class for version numbering classes.  Just provides
+    constructor (__init__) and reproducer (__repr__), because those
+    seem to be the same for all version numbering classes; and route
+    rich comparisons to _cmp.
+    """
+
+    def __init__(self, vstring=None):
+        if vstring:
+            self.parse(vstring)
+        warnings.warn(
+            "distutils Version classes are deprecated. Use packaging.version instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+
+    def __repr__(self):
+        return f"{self.__class__.__name__} ('{self}')"
+
+    def __eq__(self, other):
+        c = self._cmp(other)
+        if c is NotImplemented:
+            return c
+        return c == 0
+
+    def __lt__(self, other):
+        c = self._cmp(other)
+        if c is NotImplemented:
+            return c
+        return c < 0
+
+    def __le__(self, other):
+        c = self._cmp(other)
+        if c is NotImplemented:
+            return c
+        return c <= 0
+
+    def __gt__(self, other):
+        c = self._cmp(other)
+        if c is NotImplemented:
+            return c
+        return c > 0
+
+    def __ge__(self, other):
+        c = self._cmp(other)
+        if c is NotImplemented:
+            return c
+        return c >= 0
+
+
+# Interface for version-number classes -- must be implemented
+# by the following classes (the concrete ones -- Version should
+# be treated as an abstract class).
+#    __init__ (string) - create and take same action as 'parse'
+#                        (string parameter is optional)
+#    parse (string)    - convert a string representation to whatever
+#                        internal representation is appropriate for
+#                        this style of version numbering
+#    __str__ (self)    - convert back to a string; should be very similar
+#                        (if not identical to) the string supplied to parse
+#    __repr__ (self)   - generate Python code to recreate
+#                        the instance
+#    _cmp (self, other) - compare two version numbers ('other' may
+#                        be an unparsed version string, or another
+#                        instance of your version class)
+
+
+class StrictVersion(Version):
+    """Version numbering for anal retentives and software idealists.
+    Implements the standard interface for version number classes as
+    described above.  A version number consists of two or three
+    dot-separated numeric components, with an optional "pre-release" tag
+    on the end.  The pre-release tag consists of the letter 'a' or 'b'
+    followed by a number.  If the numeric components of two version
+    numbers are equal, then one with a pre-release tag will always
+    be deemed earlier (lesser) than one without.
+
+    The following are valid version numbers (shown in the order that
+    would be obtained by sorting according to the supplied cmp function):
+
+        0.4       0.4.0  (these two are equivalent)
+        0.4.1
+        0.5a1
+        0.5b3
+        0.5
+        0.9.6
+        1.0
+        1.0.4a3
+        1.0.4b1
+        1.0.4
+
+    The following are examples of invalid version numbers:
+
+        1
+        2.7.2.2
+        1.3.a4
+        1.3pl1
+        1.3c4
+
+    The rationale for this version numbering system will be explained
+    in the distutils documentation.
+    """
+
+    version_re = re.compile(
+        r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', re.VERBOSE | re.ASCII
+    )
+
+    def parse(self, vstring):
+        match = self.version_re.match(vstring)
+        if not match:
+            raise ValueError(f"invalid version number '{vstring}'")
+
+        (major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6)
+
+        if patch:
+            self.version = tuple(map(int, [major, minor, patch]))
+        else:
+            self.version = tuple(map(int, [major, minor])) + (0,)
+
+        if prerelease:
+            self.prerelease = (prerelease[0], int(prerelease_num))
+        else:
+            self.prerelease = None
+
+    def __str__(self):
+        if self.version[2] == 0:
+            vstring = '.'.join(map(str, self.version[0:2]))
+        else:
+            vstring = '.'.join(map(str, self.version))
+
+        if self.prerelease:
+            vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
+
+        return vstring
+
+    def _cmp(self, other):
+        if isinstance(other, str):
+            with suppress_known_deprecation():
+                other = StrictVersion(other)
+        elif not isinstance(other, StrictVersion):
+            return NotImplemented
+
+        if self.version == other.version:
+            # versions match; pre-release drives the comparison
+            return self._cmp_prerelease(other)
+
+        return -1 if self.version < other.version else 1
+
+    def _cmp_prerelease(self, other):
+        """
+        case 1: self has prerelease, other doesn't; other is greater
+        case 2: self doesn't have prerelease, other does: self is greater
+        case 3: both or neither have prerelease: compare them!
+        """
+        if self.prerelease and not other.prerelease:
+            return -1
+        elif not self.prerelease and other.prerelease:
+            return 1
+
+        if self.prerelease == other.prerelease:
+            return 0
+        elif self.prerelease < other.prerelease:
+            return -1
+        else:
+            return 1
+
+
+# end class StrictVersion
+
+
+# The rules according to Greg Stein:
+# 1) a version number has 1 or more numbers separated by a period or by
+#    sequences of letters. If only periods, then these are compared
+#    left-to-right to determine an ordering.
+# 2) sequences of letters are part of the tuple for comparison and are
+#    compared lexicographically
+# 3) recognize the numeric components may have leading zeroes
+#
+# The LooseVersion class below implements these rules: a version number
+# string is split up into a tuple of integer and string components, and
+# comparison is a simple tuple comparison.  This means that version
+# numbers behave in a predictable and obvious way, but a way that might
+# not necessarily be how people *want* version numbers to behave.  There
+# wouldn't be a problem if people could stick to purely numeric version
+# numbers: just split on period and compare the numbers as tuples.
+# However, people insist on putting letters into their version numbers;
+# the most common purpose seems to be:
+#   - indicating a "pre-release" version
+#     ('alpha', 'beta', 'a', 'b', 'pre', 'p')
+#   - indicating a post-release patch ('p', 'pl', 'patch')
+# but of course this can't cover all version number schemes, and there's
+# no way to know what a programmer means without asking him.
+#
+# The problem is what to do with letters (and other non-numeric
+# characters) in a version number.  The current implementation does the
+# obvious and predictable thing: keep them as strings and compare
+# lexically within a tuple comparison.  This has the desired effect if
+# an appended letter sequence implies something "post-release":
+# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
+#
+# However, if letters in a version number imply a pre-release version,
+# the "obvious" thing isn't correct.  Eg. you would expect that
+# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
+# implemented here, this just isn't so.
+#
+# Two possible solutions come to mind.  The first is to tie the
+# comparison algorithm to a particular set of semantic rules, as has
+# been done in the StrictVersion class above.  This works great as long
+# as everyone can go along with bondage and discipline.  Hopefully a
+# (large) subset of Python module programmers will agree that the
+# particular flavour of bondage and discipline provided by StrictVersion
+# provides enough benefit to be worth using, and will submit their
+# version numbering scheme to its domination.  The free-thinking
+# anarchists in the lot will never give in, though, and something needs
+# to be done to accommodate them.
+#
+# Perhaps a "moderately strict" version class could be implemented that
+# lets almost anything slide (syntactically), and makes some heuristic
+# assumptions about non-digits in version number strings.  This could
+# sink into special-case-hell, though; if I was as talented and
+# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
+# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
+# just as happy dealing with things like "2g6" and "1.13++".  I don't
+# think I'm smart enough to do it right though.
+#
+# In any case, I've coded the test suite for this module (see
+# ../test/test_version.py) specifically to fail on things like comparing
+# "1.2a2" and "1.2".  That's not because the *code* is doing anything
+# wrong, it's because the simple, obvious design doesn't match my
+# complicated, hairy expectations for real-world version numbers.  It
+# would be a snap to fix the test suite to say, "Yep, LooseVersion does
+# the Right Thing" (ie. the code matches the conception).  But I'd rather
+# have a conception that matches common notions about version numbers.
+
+
+class LooseVersion(Version):
+    """Version numbering for anarchists and software realists.
+    Implements the standard interface for version number classes as
+    described above.  A version number consists of a series of numbers,
+    separated by either periods or strings of letters.  When comparing
+    version numbers, the numeric components will be compared
+    numerically, and the alphabetic components lexically.  The following
+    are all valid version numbers, in no particular order:
+
+        1.5.1
+        1.5.2b2
+        161
+        3.10a
+        8.02
+        3.4j
+        1996.07.12
+        3.2.pl0
+        3.1.1.6
+        2g6
+        11g
+        0.960923
+        2.2beta29
+        1.13++
+        5.5.kw
+        2.0b1pl0
+
+    In fact, there is no such thing as an invalid version number under
+    this scheme; the rules for comparison are simple and predictable,
+    but may not always give the results you want (for some definition
+    of "want").
+    """
+
+    component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
+
+    def parse(self, vstring):
+        # I've given up on thinking I can reconstruct the version string
+        # from the parsed tuple -- so I just store the string here for
+        # use by __str__
+        self.vstring = vstring
+        components = [x for x in self.component_re.split(vstring) if x and x != '.']
+        for i, obj in enumerate(components):
+            try:
+                components[i] = int(obj)
+            except ValueError:
+                pass
+
+        self.version = components
+
+    def __str__(self):
+        return self.vstring
+
+    def __repr__(self):
+        return f"LooseVersion ('{self}')"
+
+    def _cmp(self, other):
+        if isinstance(other, str):
+            other = LooseVersion(other)
+        elif not isinstance(other, LooseVersion):
+            return NotImplemented
+
+        if self.version == other.version:
+            return 0
+        if self.version < other.version:
+            return -1
+        if self.version > other.version:
+            return 1
+
+
+# end class LooseVersion
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/versionpredicate.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/versionpredicate.py
new file mode 100644
index 00000000..fe31b0ed
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/versionpredicate.py
@@ -0,0 +1,175 @@
+"""Module for parsing and testing package version predicate strings."""
+
+import operator
+import re
+
+from . import version
+
+re_validPackage = re.compile(r"(?i)^\s*([a-z_]\w*(?:\.[a-z_]\w*)*)(.*)", re.ASCII)
+# (package) (rest)
+
+re_paren = re.compile(r"^\s*\((.*)\)\s*$")  # (list) inside of parentheses
+re_splitComparison = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$")
+# (comp) (version)
+
+
+def splitUp(pred):
+    """Parse a single version comparison.
+
+    Return (comparison string, StrictVersion)
+    """
+    res = re_splitComparison.match(pred)
+    if not res:
+        raise ValueError(f"bad package restriction syntax: {pred!r}")
+    comp, verStr = res.groups()
+    with version.suppress_known_deprecation():
+        other = version.StrictVersion(verStr)
+    return (comp, other)
+
+
+compmap = {
+    "<": operator.lt,
+    "<=": operator.le,
+    "==": operator.eq,
+    ">": operator.gt,
+    ">=": operator.ge,
+    "!=": operator.ne,
+}
+
+
+class VersionPredicate:
+    """Parse and test package version predicates.
+
+    >>> v = VersionPredicate('pyepat.abc (>1.0, <3333.3a1, !=1555.1b3)')
+
+    The `name` attribute provides the full dotted name that is given::
+
+    >>> v.name
+    'pyepat.abc'
+
+    The str() of a `VersionPredicate` provides a normalized
+    human-readable version of the expression::
+
+    >>> print(v)
+    pyepat.abc (> 1.0, < 3333.3a1, != 1555.1b3)
+
+    The `satisfied_by()` method can be used to determine with a given
+    version number is included in the set described by the version
+    restrictions::
+
+    >>> v.satisfied_by('1.1')
+    True
+    >>> v.satisfied_by('1.4')
+    True
+    >>> v.satisfied_by('1.0')
+    False
+    >>> v.satisfied_by('4444.4')
+    False
+    >>> v.satisfied_by('1555.1b3')
+    False
+
+    `VersionPredicate` is flexible in accepting extra whitespace::
+
+    >>> v = VersionPredicate(' pat( ==  0.1  )  ')
+    >>> v.name
+    'pat'
+    >>> v.satisfied_by('0.1')
+    True
+    >>> v.satisfied_by('0.2')
+    False
+
+    If any version numbers passed in do not conform to the
+    restrictions of `StrictVersion`, a `ValueError` is raised::
+
+    >>> v = VersionPredicate('p1.p2.p3.p4(>=1.0, <=1.3a1, !=1.2zb3)')
+    Traceback (most recent call last):
+      ...
+    ValueError: invalid version number '1.2zb3'
+
+    It the module or package name given does not conform to what's
+    allowed as a legal module or package name, `ValueError` is
+    raised::
+
+    >>> v = VersionPredicate('foo-bar')
+    Traceback (most recent call last):
+      ...
+    ValueError: expected parenthesized list: '-bar'
+
+    >>> v = VersionPredicate('foo bar (12.21)')
+    Traceback (most recent call last):
+      ...
+    ValueError: expected parenthesized list: 'bar (12.21)'
+
+    """
+
+    def __init__(self, versionPredicateStr):
+        """Parse a version predicate string."""
+        # Fields:
+        #    name:  package name
+        #    pred:  list of (comparison string, StrictVersion)
+
+        versionPredicateStr = versionPredicateStr.strip()
+        if not versionPredicateStr:
+            raise ValueError("empty package restriction")
+        match = re_validPackage.match(versionPredicateStr)
+        if not match:
+            raise ValueError(f"bad package name in {versionPredicateStr!r}")
+        self.name, paren = match.groups()
+        paren = paren.strip()
+        if paren:
+            match = re_paren.match(paren)
+            if not match:
+                raise ValueError(f"expected parenthesized list: {paren!r}")
+            str = match.groups()[0]
+            self.pred = [splitUp(aPred) for aPred in str.split(",")]
+            if not self.pred:
+                raise ValueError(f"empty parenthesized list in {versionPredicateStr!r}")
+        else:
+            self.pred = []
+
+    def __str__(self):
+        if self.pred:
+            seq = [cond + " " + str(ver) for cond, ver in self.pred]
+            return self.name + " (" + ", ".join(seq) + ")"
+        else:
+            return self.name
+
+    def satisfied_by(self, version):
+        """True if version is compatible with all the predicates in self.
+        The parameter version must be acceptable to the StrictVersion
+        constructor.  It may be either a string or StrictVersion.
+        """
+        for cond, ver in self.pred:
+            if not compmap[cond](version, ver):
+                return False
+        return True
+
+
+_provision_rx = None
+
+
+def split_provision(value):
+    """Return the name and optional version number of a provision.
+
+    The version number, if given, will be returned as a `StrictVersion`
+    instance, otherwise it will be `None`.
+
+    >>> split_provision('mypkg')
+    ('mypkg', None)
+    >>> split_provision(' mypkg( 1.2 ) ')
+    ('mypkg', StrictVersion ('1.2'))
+    """
+    global _provision_rx
+    if _provision_rx is None:
+        _provision_rx = re.compile(
+            r"([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)(?:\s*\(\s*([^)\s]+)\s*\))?$", re.ASCII
+        )
+    value = value.strip()
+    m = _provision_rx.match(value)
+    if not m:
+        raise ValueError(f"illegal provides specification: {value!r}")
+    ver = m.group(2) or None
+    if ver:
+        with version.suppress_known_deprecation():
+            ver = version.StrictVersion(ver)
+    return m.group(1), ver
diff --git a/.venv/lib/python3.12/site-packages/setuptools/_distutils/zosccompiler.py b/.venv/lib/python3.12/site-packages/setuptools/_distutils/zosccompiler.py
new file mode 100644
index 00000000..e49630ac
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/_distutils/zosccompiler.py
@@ -0,0 +1,3 @@
+from .compilers.C import zos
+
+zOSCCompiler = zos.Compiler