nva.recipe.rocket 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: monthly
@@ -0,0 +1,43 @@
1
+ name: release
2
+
3
+ # Publishes to PyPI when a version tag (e.g. 0.1.0) is pushed.
4
+ # Uses PyPI trusted publishing: configure a trusted publisher for this
5
+ # repository, workflow "release.yml" and environment "pypi" on PyPI first.
6
+
7
+ on:
8
+ push:
9
+ tags: ["[0-9]+.[0-9]+*"]
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v6
17
+ - name: Check that the tag matches the package version
18
+ run: |
19
+ version=$(uv run --no-project --python 3.12 python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
20
+ test "$version" = "$GITHUB_REF_NAME" || {
21
+ echo "Tag $GITHUB_REF_NAME does not match version $version in pyproject.toml"
22
+ exit 1
23
+ }
24
+ - run: uv run pytest
25
+ - run: uv build
26
+ - run: uvx twine check --strict dist/*
27
+ - uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ publish:
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ environment: pypi
36
+ permissions:
37
+ id-token: write
38
+ steps:
39
+ - uses: actions/download-artifact@v4
40
+ with:
41
+ name: dist
42
+ path: dist/
43
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,41 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test:
11
+ name: Python ${{ matrix.python }} / zc.buildout ${{ matrix.buildout }}
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
17
+ buildout: ["5", "6"]
18
+ exclude:
19
+ # zc.buildout 6 requires Python 3.10+
20
+ - python: "3.9"
21
+ buildout: "6"
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: astral-sh/setup-uv@v6
25
+ - name: Run tests
26
+ run: >
27
+ uv run --isolated --python ${{ matrix.python }}
28
+ --with "zc.buildout==${{ matrix.buildout }}.*"
29
+ pytest -v
30
+
31
+ build:
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ - uses: astral-sh/setup-uv@v6
36
+ - run: uv build
37
+ - run: uvx twine check --strict dist/*
38
+ - uses: actions/upload-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
@@ -0,0 +1,5 @@
1
+ *.egg-info/
2
+ .pytest_cache/
3
+ .venv/
4
+ __pycache__/
5
+ dist/
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-10-24)
4
+
5
+ - Initial release: parallel develop eggs, builds without build isolation
6
+ with per-package fallback, and a cache for unchanged develop eggs.
@@ -0,0 +1,45 @@
1
+ Zope Public License (ZPL) Version 2.1
2
+
3
+ A copyright notice accompanies this license document that identifies the
4
+ copyright holders.
5
+
6
+ This license has been certified as open source. It has also been designated as
7
+ GPL compatible by the Free Software Foundation (FSF).
8
+
9
+ Redistribution and use in source and binary forms, with or without
10
+ modification, are permitted provided that the following conditions are met:
11
+
12
+ 1. Redistributions in source code must retain the accompanying copyright
13
+ notice, this list of conditions, and the following disclaimer.
14
+
15
+ 2. Redistributions in binary form must reproduce the accompanying copyright
16
+ notice, this list of conditions, and the following disclaimer in the
17
+ documentation and/or other materials provided with the distribution.
18
+
19
+ 3. Names of the copyright holders must not be used to endorse or promote
20
+ products derived from this software without prior written permission from
21
+ the copyright holders.
22
+
23
+ 4. The right to distribute this software or to use it for any purpose does not
24
+ give you the right to use Servicemarks (sm) or Trademarks (tm) of the
25
+ copyright holders. Use of them is covered by separate agreement with the
26
+ copyright holders.
27
+
28
+ 5. If any files are modified, you must cause the modified files to carry
29
+ prominent notices stating that you changed the files and the date of any
30
+ change.
31
+
32
+ Disclaimer
33
+
34
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
35
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
36
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
37
+ EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
38
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
39
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
40
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
41
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
42
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
43
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44
+
45
+ Copyright (c) 2026 Novareto
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.5
2
+ Name: nva.recipe.rocket
3
+ Version: 0.1.0
4
+ Summary: zc.buildout extension: build develop eggs in parallel, without build isolation, and reuse unchanged ones
5
+ Project-URL: Homepage, https://github.com/novareto/nva.recipe.rocket
6
+ Project-URL: Issues, https://github.com/novareto/nva.recipe.rocket/issues
7
+ Project-URL: Changelog, https://github.com/novareto/nva.recipe.rocket/blob/main/CHANGES.md
8
+ Author-email: Novareto <ck@novareto.de>
9
+ License: ZPL-2.1
10
+ License-File: LICENSE.txt
11
+ Keywords: buildout,develop,extension,mr.developer,performance
12
+ Classifier: Framework :: Buildout
13
+ Classifier: Framework :: Buildout :: Extension
14
+ Classifier: License :: OSI Approved :: Zope Public License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.9
17
+ Requires-Dist: zc-buildout>=5
18
+ Description-Content-Type: text/markdown
19
+
20
+ # nva.recipe.rocket
21
+
22
+ [![tests](https://github.com/novareto/nva.recipe.rocket/actions/workflows/tests.yml/badge.svg)](https://github.com/novareto/nva.recipe.rocket/actions/workflows/tests.yml)
23
+
24
+ A [zc.buildout](https://github.com/buildout/buildout) extension that makes the
25
+ **develop step** fast.
26
+
27
+ Since zc.buildout 5, every develop egg — every `develop =` entry and every
28
+ [mr.developer](https://pypi.org/project/mr.developer/) auto-checkout — is
29
+ installed with `pip install -e` on **every** buildout run: one after the
30
+ other, each in a fresh isolated build environment (pip installs setuptools
31
+ into a temporary venv first). With 20+ checkouts that alone takes 40+ seconds,
32
+ even when nothing changed.
33
+
34
+ `nva.recipe.rocket` replaces that step:
35
+
36
+ - **Parallel** — develop eggs are built concurrently, one worker per CPU.
37
+ - **No build isolation** — `pip install -e --no-build-isolation` uses the
38
+ setuptools buildout already runs with. If that fails for a package (for
39
+ example its build backend, like hatchling, is not installed), only that
40
+ package is retried with isolation.
41
+ - **Cache** — the files a develop egg generates in `develop-eggs/` are cached.
42
+ On the next run they are copied back instead of rebuilt, as long as the
43
+ package's `setup.py`, `setup.cfg`, `pyproject.toml` and its top-level
44
+ package layout are unchanged. Code changes never need a rebuild — editable
45
+ installs point at the source directory.
46
+
47
+ Measured on a project with 22 mr.developer checkouts:
48
+
49
+ | develop step | time |
50
+ |---------------------------------------|-------|
51
+ | zc.buildout 6, plain | ~44 s |
52
+ | without build isolation | ~18 s |
53
+ | + parallel (10 workers) | ~5 s |
54
+ | + cache, nothing changed | < 1 s |
55
+
56
+ ## Usage
57
+
58
+ ```ini
59
+ [buildout]
60
+ extensions =
61
+ mr.developer
62
+ nva.recipe.rocket
63
+ ```
64
+
65
+ The order of extensions does not matter. Requires zc.buildout 5 or newer.
66
+
67
+ ## Options
68
+
69
+ All options go into the `[buildout]` section and can be set on the command
70
+ line, e.g. `bin/buildout buildout:rocket-cache=false`.
71
+
72
+ | option | default | meaning |
73
+ |---------------------------|-----------------------|---------|
74
+ | `rocket-workers` | number of CPUs | parallel builds; `1` builds sequentially |
75
+ | `rocket-build-isolation` | `false` | `true` builds with pip build isolation, like plain buildout |
76
+ | `rocket-cache` | `true` | reuse the files of unchanged develop eggs |
77
+ | `rocket-cache-directory` | `${buildout:parts-directory}/.rocket` | where the cache lives |
78
+
79
+ ## When is a develop egg rebuilt?
80
+
81
+ The cache key of a develop egg is made from:
82
+
83
+ - the contents of `setup.py`, `setup.cfg` and `pyproject.toml`,
84
+ - the names of the top-level packages and modules (flat layout and src
85
+ layout, two levels deep for namespace packages) and the `*.egg-info`
86
+ directory,
87
+ - the Python interpreter, the zc.buildout, setuptools and pip versions,
88
+ - the `rocket-build-isolation` setting.
89
+
90
+ Anything else that affects the generated metadata is **not** seen, for
91
+ example a version computed from git tags (setuptools-scm). Run once with
92
+ `buildout:rocket-cache=false`, or delete `parts/.rocket`, to force a rebuild.
93
+
94
+ ## Other things that make buildout slow
95
+
96
+ Not handled by this extension, but worth checking in the buildout output:
97
+
98
+ - `Scanning index of all packages (this may take a while)` — an unpinned
99
+ package was not found on its index page, so setuptools downloads and parses
100
+ the whole PyPI `/simple/` root page (~1 million links), once per part. Pin
101
+ the package listed in `Couldn't retrieve index page for ...`.
102
+ - `Upgraded: setuptools version ...; Restarting.` on every run — the
103
+ setuptools pinned in `[versions]` differs from the one installed in the
104
+ virtualenv that runs buildout. Make them match.
105
+ - `PIP_NO_BUILD_ISOLATION=0` also speeds up plain buildout (yes, `0`: pip's
106
+ environment variable is inverted for historical reasons).
107
+
108
+ ## Development
109
+
110
+ ```sh
111
+ uv sync
112
+ uv run pytest
113
+ ```
@@ -0,0 +1,94 @@
1
+ # nva.recipe.rocket
2
+
3
+ [![tests](https://github.com/novareto/nva.recipe.rocket/actions/workflows/tests.yml/badge.svg)](https://github.com/novareto/nva.recipe.rocket/actions/workflows/tests.yml)
4
+
5
+ A [zc.buildout](https://github.com/buildout/buildout) extension that makes the
6
+ **develop step** fast.
7
+
8
+ Since zc.buildout 5, every develop egg — every `develop =` entry and every
9
+ [mr.developer](https://pypi.org/project/mr.developer/) auto-checkout — is
10
+ installed with `pip install -e` on **every** buildout run: one after the
11
+ other, each in a fresh isolated build environment (pip installs setuptools
12
+ into a temporary venv first). With 20+ checkouts that alone takes 40+ seconds,
13
+ even when nothing changed.
14
+
15
+ `nva.recipe.rocket` replaces that step:
16
+
17
+ - **Parallel** — develop eggs are built concurrently, one worker per CPU.
18
+ - **No build isolation** — `pip install -e --no-build-isolation` uses the
19
+ setuptools buildout already runs with. If that fails for a package (for
20
+ example its build backend, like hatchling, is not installed), only that
21
+ package is retried with isolation.
22
+ - **Cache** — the files a develop egg generates in `develop-eggs/` are cached.
23
+ On the next run they are copied back instead of rebuilt, as long as the
24
+ package's `setup.py`, `setup.cfg`, `pyproject.toml` and its top-level
25
+ package layout are unchanged. Code changes never need a rebuild — editable
26
+ installs point at the source directory.
27
+
28
+ Measured on a project with 22 mr.developer checkouts:
29
+
30
+ | develop step | time |
31
+ |---------------------------------------|-------|
32
+ | zc.buildout 6, plain | ~44 s |
33
+ | without build isolation | ~18 s |
34
+ | + parallel (10 workers) | ~5 s |
35
+ | + cache, nothing changed | < 1 s |
36
+
37
+ ## Usage
38
+
39
+ ```ini
40
+ [buildout]
41
+ extensions =
42
+ mr.developer
43
+ nva.recipe.rocket
44
+ ```
45
+
46
+ The order of extensions does not matter. Requires zc.buildout 5 or newer.
47
+
48
+ ## Options
49
+
50
+ All options go into the `[buildout]` section and can be set on the command
51
+ line, e.g. `bin/buildout buildout:rocket-cache=false`.
52
+
53
+ | option | default | meaning |
54
+ |---------------------------|-----------------------|---------|
55
+ | `rocket-workers` | number of CPUs | parallel builds; `1` builds sequentially |
56
+ | `rocket-build-isolation` | `false` | `true` builds with pip build isolation, like plain buildout |
57
+ | `rocket-cache` | `true` | reuse the files of unchanged develop eggs |
58
+ | `rocket-cache-directory` | `${buildout:parts-directory}/.rocket` | where the cache lives |
59
+
60
+ ## When is a develop egg rebuilt?
61
+
62
+ The cache key of a develop egg is made from:
63
+
64
+ - the contents of `setup.py`, `setup.cfg` and `pyproject.toml`,
65
+ - the names of the top-level packages and modules (flat layout and src
66
+ layout, two levels deep for namespace packages) and the `*.egg-info`
67
+ directory,
68
+ - the Python interpreter, the zc.buildout, setuptools and pip versions,
69
+ - the `rocket-build-isolation` setting.
70
+
71
+ Anything else that affects the generated metadata is **not** seen, for
72
+ example a version computed from git tags (setuptools-scm). Run once with
73
+ `buildout:rocket-cache=false`, or delete `parts/.rocket`, to force a rebuild.
74
+
75
+ ## Other things that make buildout slow
76
+
77
+ Not handled by this extension, but worth checking in the buildout output:
78
+
79
+ - `Scanning index of all packages (this may take a while)` — an unpinned
80
+ package was not found on its index page, so setuptools downloads and parses
81
+ the whole PyPI `/simple/` root page (~1 million links), once per part. Pin
82
+ the package listed in `Couldn't retrieve index page for ...`.
83
+ - `Upgraded: setuptools version ...; Restarting.` on every run — the
84
+ setuptools pinned in `[versions]` differs from the one installed in the
85
+ virtualenv that runs buildout. Make them match.
86
+ - `PIP_NO_BUILD_ISOLATION=0` also speeds up plain buildout (yes, `0`: pip's
87
+ environment variable is inverted for historical reasons).
88
+
89
+ ## Development
90
+
91
+ ```sh
92
+ uv sync
93
+ uv run pytest
94
+ ```
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nva.recipe.rocket"
7
+ version = "0.1.0"
8
+ description = "zc.buildout extension: build develop eggs in parallel, without build isolation, and reuse unchanged ones"
9
+ readme = "README.md"
10
+ license = { text = "ZPL-2.1" }
11
+ license-files = ["LICENSE.txt"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Novareto", email = "ck@novareto.de" }]
14
+ keywords = ["buildout", "extension", "develop", "mr.developer", "performance"]
15
+ classifiers = [
16
+ "Framework :: Buildout",
17
+ "Framework :: Buildout :: Extension",
18
+ "Programming Language :: Python :: 3",
19
+ "License :: OSI Approved :: Zope Public License",
20
+ ]
21
+ dependencies = ["zc.buildout>=5"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/novareto/nva.recipe.rocket"
25
+ Issues = "https://github.com/novareto/nva.recipe.rocket/issues"
26
+ Changelog = "https://github.com/novareto/nva.recipe.rocket/blob/main/CHANGES.md"
27
+
28
+ [project.entry-points."zc.buildout.extension"]
29
+ default = "nva.recipe.rocket:extension"
30
+
31
+ [dependency-groups]
32
+ dev = ["pytest", "setuptools<81", "pip"]
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/nva"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
@@ -0,0 +1,314 @@
1
+ """zc.buildout extension that speeds up the ``develop`` step.
2
+
3
+ zc.buildout 5+ runs ``pip install -e`` for every develop egg (for example
4
+ every mr.developer auto-checkout) on every run, one after the other, each in
5
+ a fresh isolated build environment. This extension replaces that step:
6
+
7
+ * develop eggs are built in parallel (``rocket-workers``),
8
+ * builds run without pip build isolation, falling back to an isolated build
9
+ per package if that fails (``rocket-build-isolation``),
10
+ * the generated files of every develop egg are cached and restored as long
11
+ as its packaging metadata did not change (``rocket-cache``).
12
+ """
13
+
14
+ import glob
15
+ import hashlib
16
+ import logging
17
+ import os
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+ import threading
23
+ import time
24
+ import types
25
+ from concurrent.futures import ThreadPoolExecutor
26
+ from dataclasses import dataclass
27
+ from importlib.metadata import PackageNotFoundError, version
28
+ from pathlib import Path
29
+
30
+ import zc.buildout.easy_install as easy_install
31
+
32
+ logger = logging.getLogger("nva.recipe.rocket")
33
+
34
+ #: Bump to invalidate all existing cache entries.
35
+ CACHE_FORMAT = "1"
36
+
37
+ #: Files whose content decides whether a develop egg must be rebuilt.
38
+ KEY_FILES = ("setup.py", "setup.cfg", "pyproject.toml")
39
+
40
+ #: Marker written into a cache entry once it is complete.
41
+ COMPLETE = ".rocket-complete"
42
+
43
+ _local = threading.local()
44
+ _get_subprocess_output = easy_install.get_subprocess_output
45
+
46
+
47
+ class NoIsolationFailed(Exception):
48
+ """A build without build isolation failed; retry with isolation."""
49
+
50
+ def __init__(self, output):
51
+ super().__init__("build without isolation failed")
52
+ self.output = output
53
+
54
+
55
+ def _bool(value):
56
+ return value.strip().lower() in ("true", "yes", "on", "1")
57
+
58
+
59
+ @dataclass
60
+ class Settings:
61
+ workers: int
62
+ build_isolation: bool
63
+ cache: bool
64
+ cache_directory: Path
65
+
66
+ @classmethod
67
+ def from_buildout(cls, buildout):
68
+ options = buildout["buildout"]
69
+ workers = int(options.get("rocket-workers", "0") or 0)
70
+ if workers < 1:
71
+ workers = os.cpu_count() or 1
72
+ cache_directory = options.get("rocket-cache-directory") or os.path.join(
73
+ options["parts-directory"], ".rocket"
74
+ )
75
+ return cls(
76
+ workers=workers,
77
+ build_isolation=_bool(options.get("rocket-build-isolation", "false")),
78
+ cache=_bool(options.get("rocket-cache", "true")),
79
+ cache_directory=Path(cache_directory),
80
+ )
81
+
82
+
83
+ def get_subprocess_output(args, **kw):
84
+ """Wrap buildout's helper to build without isolation in worker threads.
85
+
86
+ Only the develop workers set the thread-local flag, so regular egg
87
+ installs in the main thread are left alone. A failure is raised as
88
+ :class:`NoIsolationFailed` without printing pip's output, because the
89
+ build is retried with isolation.
90
+ """
91
+ if not getattr(_local, "no_isolation", False):
92
+ return _get_subprocess_output(args, **kw)
93
+ args = list(args)
94
+ args.insert(args.index("install") + 1, "--no-build-isolation")
95
+ result = subprocess.run(
96
+ args, **kw, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
97
+ )
98
+ output = result.stdout.decode("utf-8", "replace")
99
+ if result.returncode:
100
+ raise NoIsolationFailed(output)
101
+ return output
102
+
103
+
104
+ def _project_directory(setup):
105
+ if not os.path.isdir(setup):
106
+ setup = os.path.dirname(setup)
107
+ return Path(setup).expanduser().resolve()
108
+
109
+
110
+ def _listing(directory):
111
+ """Names that influence an editable install: top-level packages/modules.
112
+
113
+ Covers the flat layout and the src layout two levels deep (namespace
114
+ packages), plus the ``*.egg-info`` directory setuptools writes there.
115
+ """
116
+ names = []
117
+ for base in (directory, directory / "src"):
118
+ if not base.is_dir():
119
+ continue
120
+ for entry in sorted(base.iterdir()):
121
+ if entry.name.startswith(".") or entry.name == "__pycache__":
122
+ continue
123
+ if entry.is_dir():
124
+ names.append(str(entry.relative_to(directory)) + "/")
125
+ if base.name == "src":
126
+ names.extend(
127
+ str(sub.relative_to(directory)) + "/"
128
+ for sub in sorted(entry.iterdir())
129
+ if sub.is_dir() and sub.name != "__pycache__"
130
+ )
131
+ elif entry.suffix == ".py":
132
+ names.append(str(entry.relative_to(directory)))
133
+ return names
134
+
135
+
136
+ def _dist_version(name):
137
+ try:
138
+ return version(name)
139
+ except PackageNotFoundError:
140
+ return ""
141
+
142
+
143
+ def cache_key(directory, settings):
144
+ digest = hashlib.sha256()
145
+
146
+ def add(*parts):
147
+ for part in parts:
148
+ digest.update(part if isinstance(part, bytes) else part.encode())
149
+ digest.update(b"\0")
150
+
151
+ add(CACHE_FORMAT, sys.executable, sys.version, str(directory))
152
+ add(*(_dist_version(n) for n in ("zc.buildout", "setuptools", "pip")))
153
+ add(str(settings.build_isolation))
154
+ for name in KEY_FILES:
155
+ path = directory / name
156
+ add(name, path.read_bytes() if path.is_file() else b"-")
157
+ add(*_listing(directory))
158
+ return digest.hexdigest()
159
+
160
+
161
+ def _entry_prefix(directory):
162
+ path_hash = hashlib.sha256(str(directory).encode()).hexdigest()[:8]
163
+ return f"{directory.name}-{path_hash}-"
164
+
165
+
166
+ def _copy_into(source, dest):
167
+ """Copy the generated files from ``source`` into ``dest``."""
168
+ copied = []
169
+ for entry in sorted(os.listdir(source)):
170
+ if entry.startswith(".rocket"):
171
+ continue
172
+ src, target = os.path.join(source, entry), os.path.join(dest, entry)
173
+ if os.path.isdir(target) and not os.path.islink(target):
174
+ shutil.rmtree(target)
175
+ elif os.path.lexists(target):
176
+ os.remove(target)
177
+ if os.path.isdir(src):
178
+ shutil.copytree(src, target, symlinks=True)
179
+ else:
180
+ shutil.copy2(src, target)
181
+ copied.append(target)
182
+ return copied
183
+
184
+
185
+ def _build(setup, staging, settings):
186
+ if not settings.build_isolation:
187
+ _local.no_isolation = True
188
+ try:
189
+ easy_install.develop(setup, str(staging))
190
+ return
191
+ except NoIsolationFailed as error:
192
+ logger.info(
193
+ "Building %r without build isolation failed, "
194
+ "retrying with isolation.",
195
+ setup,
196
+ )
197
+ logger.debug(error.output)
198
+ for entry in staging.iterdir():
199
+ if entry.is_dir():
200
+ shutil.rmtree(entry)
201
+ else:
202
+ entry.unlink()
203
+ finally:
204
+ _local.no_isolation = False
205
+ easy_install.develop(setup, str(staging))
206
+
207
+
208
+ def develop_one(setup, dest, settings):
209
+ """Develop one egg into ``dest``; return True if it came from the cache."""
210
+ directory = _project_directory(setup)
211
+ prefix = _entry_prefix(directory)
212
+ cache_directory = settings.cache_directory
213
+ cache_directory.mkdir(parents=True, exist_ok=True)
214
+
215
+ if settings.cache:
216
+ entry = cache_directory / (prefix + cache_key(directory, settings)[:16])
217
+ if (entry / COMPLETE).is_file():
218
+ _copy_into(entry, dest)
219
+ return True
220
+
221
+ staging = Path(tempfile.mkdtemp(prefix=".tmp-" + prefix, dir=cache_directory))
222
+ try:
223
+ _build(setup, staging, settings)
224
+ _copy_into(staging, dest)
225
+ if settings.cache:
226
+ # Key again: the build may have created the *.egg-info directory.
227
+ entry = cache_directory / (prefix + cache_key(directory, settings)[:16])
228
+ for old in cache_directory.glob(prefix + "*"):
229
+ shutil.rmtree(old, ignore_errors=True)
230
+ (staging / COMPLETE).write_text(str(directory) + "\n")
231
+ staging.rename(entry)
232
+ finally:
233
+ if staging.exists():
234
+ shutil.rmtree(staging, ignore_errors=True)
235
+ return False
236
+
237
+
238
+ def _develop(self, settings):
239
+ """Replacement for ``zc.buildout.buildout.Buildout._develop``."""
240
+ __doing__ = "Processing directories listed in the develop option" # noqa
241
+
242
+ develop = self["buildout"].get("develop")
243
+ if not develop:
244
+ return ""
245
+
246
+ dest = self["buildout"]["develop-eggs-directory"]
247
+ old_files = os.listdir(dest)
248
+
249
+ setups = []
250
+ for setup in develop.split():
251
+ setup = self._buildout_path(setup)
252
+ files = glob.glob(setup)
253
+ if not files:
254
+ self._logger.warning("Couldn't develop %r (not found)", setup)
255
+ setups.extend(sorted(files))
256
+
257
+ here = os.getcwd()
258
+ started = time.monotonic()
259
+ cached = 0
260
+ pool = ThreadPoolExecutor(max_workers=settings.workers)
261
+ try:
262
+ try:
263
+ futures = [
264
+ (setup, pool.submit(develop_one, setup, dest, settings))
265
+ for setup in setups
266
+ ]
267
+ for setup, future in futures:
268
+ if future.result():
269
+ cached += 1
270
+ self._logger.info("Develop: %r (cached)", setup)
271
+ else:
272
+ self._logger.info("Develop: %r", setup)
273
+ pool.shutdown()
274
+ except Exception:
275
+ pool.shutdown(cancel_futures=True)
276
+ # Roll back, like buildout does: remove any files we created.
277
+ self._sanity_check_develop_eggs_files(dest, old_files)
278
+ self._uninstall(
279
+ "\n".join(
280
+ os.path.join(dest, f)
281
+ for f in os.listdir(dest)
282
+ if f not in old_files
283
+ )
284
+ )
285
+ raise
286
+
287
+ self._sanity_check_develop_eggs_files(dest, old_files)
288
+ logger.info(
289
+ "Developed %d eggs in %.1fs (%d built, %d cached, %d workers).",
290
+ len(setups),
291
+ time.monotonic() - started,
292
+ len(setups) - cached,
293
+ cached,
294
+ settings.workers,
295
+ )
296
+ return "\n".join(
297
+ sorted(os.path.join(dest, f) for f in os.listdir(dest) if f not in old_files)
298
+ )
299
+ finally:
300
+ os.chdir(here)
301
+
302
+
303
+ def extension(buildout):
304
+ """Entry point for ``extensions = nva.recipe.rocket``."""
305
+ if not hasattr(easy_install, "call_pip_install"):
306
+ logger.warning(
307
+ "nva.recipe.rocket needs zc.buildout 5 or newer; not activated."
308
+ )
309
+ return
310
+ settings = Settings.from_buildout(buildout)
311
+ easy_install.get_subprocess_output = get_subprocess_output
312
+ buildout._develop = types.MethodType(
313
+ lambda self: _develop(self, settings), buildout
314
+ )
@@ -0,0 +1,168 @@
1
+ import os
2
+ import textwrap
3
+
4
+ import pytest
5
+ import zc.buildout.buildout
6
+ import zc.buildout.configparser
7
+ import zc.buildout.easy_install as easy_install
8
+
9
+ import nva.recipe.rocket as rocket
10
+
11
+
12
+ def make_package(base, name, src_layout=False):
13
+ package = base / name
14
+ code = package / "src" / name if src_layout else package / name
15
+ code.mkdir(parents=True)
16
+ (code / "__init__.py").write_text("VALUE = %r\n" % name)
17
+ (package / "setup.py").write_text(
18
+ textwrap.dedent(
19
+ f"""\
20
+ from setuptools import setup, find_packages
21
+ setup(
22
+ name={name!r},
23
+ version="1.0",
24
+ packages=find_packages({'"src"' if src_layout else ''}),
25
+ {'package_dir={"": "src"},' if src_layout else ''}
26
+ )
27
+ """
28
+ )
29
+ )
30
+ return package
31
+
32
+
33
+ @pytest.fixture
34
+ def project(tmp_path):
35
+ make_package(tmp_path, "pkga")
36
+ make_package(tmp_path, "pkgb", src_layout=True)
37
+ (tmp_path / "buildout.cfg").write_text(
38
+ textwrap.dedent(
39
+ """\
40
+ [buildout]
41
+ develop = pkga pkgb
42
+ parts =
43
+ offline = true
44
+ newest = false
45
+ """
46
+ )
47
+ )
48
+ return tmp_path
49
+
50
+
51
+ @pytest.fixture
52
+ def develop_calls(monkeypatch):
53
+ calls = []
54
+ original = easy_install.develop
55
+
56
+ def develop(setup, dest, *args, **kw):
57
+ calls.append(os.path.basename(setup))
58
+ return original(setup, dest, *args, **kw)
59
+
60
+ monkeypatch.setattr(easy_install, "develop", develop)
61
+ return calls
62
+
63
+
64
+ def run_buildout(project, *options):
65
+ """Run ``bin/buildout`` in-process; options look like ``section:key=value``."""
66
+ cloptions = []
67
+ for option in options:
68
+ section, assignment = option.split(":", 1)
69
+ cloptions.append((section, *assignment.split("=", 1)))
70
+ cwd = os.getcwd()
71
+ os.chdir(project)
72
+ try:
73
+ buildout = zc.buildout.buildout.Buildout(
74
+ str(project / "buildout.cfg"), cloptions, use_user_defaults=False
75
+ )
76
+ rocket.extension(buildout)
77
+ buildout.install([])
78
+ finally:
79
+ os.chdir(cwd)
80
+
81
+
82
+ def installed_develop_eggs(project):
83
+ with open(project / ".installed.cfg") as fp:
84
+ sections = zc.buildout.configparser.parse(fp, ".installed.cfg")
85
+ return sections["buildout"]["installed_develop_eggs"].split()
86
+
87
+
88
+ def test_builds_then_reuses_cache(project, develop_calls):
89
+ run_buildout(project)
90
+ assert sorted(develop_calls) == ["pkga", "pkgb"]
91
+ first = installed_develop_eggs(project)
92
+ assert any("pkga" in p for p in first) and any("pkgb" in p for p in first)
93
+
94
+ develop_calls.clear()
95
+ run_buildout(project)
96
+ assert develop_calls == []
97
+ # Buildout removed the old files before developing; they are restored.
98
+ assert installed_develop_eggs(project) == first
99
+ assert all(os.path.exists(p) for p in first)
100
+
101
+
102
+ def test_changed_metadata_rebuilds_only_that_package(project, develop_calls):
103
+ run_buildout(project)
104
+ develop_calls.clear()
105
+
106
+ setup_py = project / "pkga" / "setup.py"
107
+ setup_py.write_text(setup_py.read_text().replace('"1.0"', '"1.1"'))
108
+ run_buildout(project)
109
+ assert develop_calls == ["pkga"]
110
+
111
+
112
+ def test_new_top_level_package_rebuilds(project, develop_calls):
113
+ run_buildout(project)
114
+ develop_calls.clear()
115
+
116
+ (project / "pkgb" / "src" / "pkgb_extra").mkdir()
117
+ run_buildout(project)
118
+ assert develop_calls == ["pkgb"]
119
+
120
+
121
+ def test_cache_can_be_disabled(project, develop_calls):
122
+ run_buildout(project, "buildout:rocket-cache=false")
123
+ run_buildout(project, "buildout:rocket-cache=false")
124
+ assert sorted(develop_calls) == ["pkga", "pkga", "pkgb", "pkgb"]
125
+
126
+
127
+ def test_sequential_worker(project, develop_calls):
128
+ run_buildout(project, "buildout:rocket-workers=1")
129
+ assert sorted(develop_calls) == ["pkga", "pkgb"]
130
+
131
+
132
+ def test_falls_back_to_build_isolation(project, monkeypatch):
133
+ attempts = []
134
+ original = easy_install.develop
135
+
136
+ def develop(setup, dest, *args, **kw):
137
+ isolated = not getattr(rocket._local, "no_isolation", False)
138
+ attempts.append((os.path.basename(setup), isolated))
139
+ if not isolated and setup.endswith("pkga"):
140
+ raise rocket.NoIsolationFailed("backend missing")
141
+ # Build every package without isolation, we are offline.
142
+ rocket._local.no_isolation = True
143
+ try:
144
+ return original(setup, dest, *args, **kw)
145
+ finally:
146
+ rocket._local.no_isolation = not isolated
147
+
148
+ monkeypatch.setattr(easy_install, "develop", develop)
149
+ run_buildout(project, "buildout:rocket-workers=1")
150
+ assert attempts == [("pkga", False), ("pkga", True), ("pkgb", False)]
151
+
152
+
153
+ def test_no_isolation_flag_only_in_worker_threads(monkeypatch):
154
+ seen = []
155
+ monkeypatch.setattr(
156
+ rocket, "_get_subprocess_output", lambda args, **kw: seen.append(args)
157
+ )
158
+ rocket.get_subprocess_output(["python", "-m", "pip", "install", "x"])
159
+ assert seen == [["python", "-m", "pip", "install", "x"]]
160
+
161
+
162
+ def test_settings_defaults(tmp_path):
163
+ buildout = {"buildout": {"parts-directory": str(tmp_path / "parts")}}
164
+ settings = rocket.Settings.from_buildout(buildout)
165
+ assert settings.workers == (os.cpu_count() or 1)
166
+ assert settings.build_isolation is False
167
+ assert settings.cache is True
168
+ assert settings.cache_directory == tmp_path / "parts" / ".rocket"
@@ -0,0 +1,358 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.9"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.11'",
6
+ "python_full_version == '3.10.*'",
7
+ "python_full_version < '3.10'",
8
+ ]
9
+
10
+ [[package]]
11
+ name = "colorama"
12
+ version = "0.4.6"
13
+ source = { registry = "https://pypi.org/simple" }
14
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
15
+ wheels = [
16
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
17
+ ]
18
+
19
+ [[package]]
20
+ name = "exceptiongroup"
21
+ version = "1.3.1"
22
+ source = { registry = "https://pypi.org/simple" }
23
+ dependencies = [
24
+ { name = "typing-extensions" },
25
+ ]
26
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
27
+ wheels = [
28
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
29
+ ]
30
+
31
+ [[package]]
32
+ name = "iniconfig"
33
+ version = "2.1.0"
34
+ source = { registry = "https://pypi.org/simple" }
35
+ resolution-markers = [
36
+ "python_full_version < '3.10'",
37
+ ]
38
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
39
+ wheels = [
40
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
41
+ ]
42
+
43
+ [[package]]
44
+ name = "iniconfig"
45
+ version = "2.3.1"
46
+ source = { registry = "https://pypi.org/simple" }
47
+ resolution-markers = [
48
+ "python_full_version >= '3.11'",
49
+ "python_full_version == '3.10.*'",
50
+ ]
51
+ sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
52
+ wheels = [
53
+ { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
54
+ ]
55
+
56
+ [[package]]
57
+ name = "nva-recipe-rocket"
58
+ version = "0.1.0"
59
+ source = { editable = "." }
60
+ dependencies = [
61
+ { name = "zc-buildout", version = "5.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
62
+ { name = "zc-buildout", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
63
+ ]
64
+
65
+ [package.dev-dependencies]
66
+ dev = [
67
+ { name = "pip", version = "26.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
68
+ { name = "pip", version = "26.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
69
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
70
+ { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
71
+ { name = "setuptools" },
72
+ ]
73
+
74
+ [package.metadata]
75
+ requires-dist = [{ name = "zc-buildout", specifier = ">=5" }]
76
+
77
+ [package.metadata.requires-dev]
78
+ dev = [
79
+ { name = "pip" },
80
+ { name = "pytest" },
81
+ { name = "setuptools", specifier = "<81" },
82
+ ]
83
+
84
+ [[package]]
85
+ name = "packaging"
86
+ version = "26.3"
87
+ source = { registry = "https://pypi.org/simple" }
88
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
89
+ wheels = [
90
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
91
+ ]
92
+
93
+ [[package]]
94
+ name = "pip"
95
+ version = "26.0.1"
96
+ source = { registry = "https://pypi.org/simple" }
97
+ resolution-markers = [
98
+ "python_full_version < '3.10'",
99
+ ]
100
+ sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" }
101
+ wheels = [
102
+ { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" },
103
+ ]
104
+
105
+ [[package]]
106
+ name = "pip"
107
+ version = "26.2.2"
108
+ source = { registry = "https://pypi.org/simple" }
109
+ resolution-markers = [
110
+ "python_full_version >= '3.11'",
111
+ "python_full_version == '3.10.*'",
112
+ ]
113
+ sdist = { url = "https://files.pythonhosted.org/packages/fa/e8/0842d757e483e895e670f655084cfe8b8c72580c6769a76b30b2789a0608/pip-26.2.2.tar.gz", hash = "sha256:3a8e5c800492787f33a4114ef54444546c340df428974ebba17e0fd47e11af2f", size = 1863336, upload-time = "2026-10-20T16:36:29.008Z" }
114
+ wheels = [
115
+ { url = "https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl", hash = "sha256:fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70", size = 1819850, upload-time = "2026-10-20T16:36:27.046Z" },
116
+ ]
117
+
118
+ [[package]]
119
+ name = "platformdirs"
120
+ version = "4.12.4"
121
+ source = { registry = "https://pypi.org/simple" }
122
+ resolution-markers = [
123
+ "python_full_version == '3.10.*'",
124
+ ]
125
+ sdist = { url = "https://files.pythonhosted.org/packages/90/a1/d5f9002a70298c64a789779077d8dd90c10aa1f47fe40c86802df874f2a6/platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250", size = 60010, upload-time = "2026-10-07T23:30:32.426Z" }
126
+ wheels = [
127
+ { url = "https://files.pythonhosted.org/packages/f4/ba/223e00b885e960edd5d4b4d178c88acce019bd5b83786093681b8c393492/platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7", size = 32596, upload-time = "2026-10-07T23:30:30.825Z" },
128
+ ]
129
+
130
+ [[package]]
131
+ name = "platformdirs"
132
+ version = "4.13.5"
133
+ source = { registry = "https://pypi.org/simple" }
134
+ resolution-markers = [
135
+ "python_full_version >= '3.11'",
136
+ ]
137
+ sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" }
138
+ wheels = [
139
+ { url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" },
140
+ ]
141
+
142
+ [[package]]
143
+ name = "pluggy"
144
+ version = "1.6.0"
145
+ source = { registry = "https://pypi.org/simple" }
146
+ resolution-markers = [
147
+ "python_full_version < '3.10'",
148
+ ]
149
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
150
+ wheels = [
151
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
152
+ ]
153
+
154
+ [[package]]
155
+ name = "pluggy"
156
+ version = "1.7.0"
157
+ source = { registry = "https://pypi.org/simple" }
158
+ resolution-markers = [
159
+ "python_full_version >= '3.11'",
160
+ "python_full_version == '3.10.*'",
161
+ ]
162
+ sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" }
163
+ wheels = [
164
+ { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" },
165
+ ]
166
+
167
+ [[package]]
168
+ name = "pygments"
169
+ version = "2.21.0"
170
+ source = { registry = "https://pypi.org/simple" }
171
+ sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
172
+ wheels = [
173
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
174
+ ]
175
+
176
+ [[package]]
177
+ name = "pytest"
178
+ version = "8.4.2"
179
+ source = { registry = "https://pypi.org/simple" }
180
+ resolution-markers = [
181
+ "python_full_version < '3.10'",
182
+ ]
183
+ dependencies = [
184
+ { name = "colorama", marker = "sys_platform == 'win32'" },
185
+ { name = "exceptiongroup" },
186
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" } },
187
+ { name = "packaging" },
188
+ { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" } },
189
+ { name = "pygments" },
190
+ { name = "tomli" },
191
+ ]
192
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
193
+ wheels = [
194
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
195
+ ]
196
+
197
+ [[package]]
198
+ name = "pytest"
199
+ version = "9.1.1"
200
+ source = { registry = "https://pypi.org/simple" }
201
+ resolution-markers = [
202
+ "python_full_version >= '3.11'",
203
+ "python_full_version == '3.10.*'",
204
+ ]
205
+ dependencies = [
206
+ { name = "colorama", marker = "sys_platform == 'win32'" },
207
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
208
+ { name = "iniconfig", version = "2.3.1", source = { registry = "https://pypi.org/simple" } },
209
+ { name = "packaging" },
210
+ { name = "pluggy", version = "1.7.0", source = { registry = "https://pypi.org/simple" } },
211
+ { name = "pygments" },
212
+ { name = "tomli", marker = "python_full_version < '3.11'" },
213
+ ]
214
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
215
+ wheels = [
216
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
217
+ ]
218
+
219
+ [[package]]
220
+ name = "setuptools"
221
+ version = "80.10.2"
222
+ source = { registry = "https://pypi.org/simple" }
223
+ sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" }
224
+ wheels = [
225
+ { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" },
226
+ ]
227
+
228
+ [[package]]
229
+ name = "tomli"
230
+ version = "2.5.0"
231
+ source = { registry = "https://pypi.org/simple" }
232
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" }
233
+ wheels = [
234
+ { url = "https://files.pythonhosted.org/packages/22/a6/ab99b60ee52acd949684febabc3005d0045d0f66bebd9cdebd67372d26dd/tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545", size = 163901, upload-time = "2026-10-07T12:22:15.601Z" },
235
+ { url = "https://files.pythonhosted.org/packages/bc/00/ee01b7ed4579180fff07142d290257f25ba786f23f3ec6005f620933c2f5/tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef", size = 163756, upload-time = "2026-10-07T12:22:16.957Z" },
236
+ { url = "https://files.pythonhosted.org/packages/72/c2/4efebf65372f6583185f79799312109dddb61102d47e5c33dcfd1a297aca/tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b", size = 268038, upload-time = "2026-10-07T12:22:18.135Z" },
237
+ { url = "https://files.pythonhosted.org/packages/53/07/5850468e925d898abb36038666f9c333a94d2a223e802a8ba5b6d319d23f/tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56", size = 276422, upload-time = "2026-10-07T12:22:19.567Z" },
238
+ { url = "https://files.pythonhosted.org/packages/b4/87/f293984cdcf83c054196d4fd3dad44fc68ae55b4b8c44bc76cef360c3150/tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1", size = 272616, upload-time = "2026-10-07T12:22:20.794Z" },
239
+ { url = "https://files.pythonhosted.org/packages/ce/ce/db582886b3c1219d3fec93ebd669332482e5aee7a91e0f7838d84f2d1759/tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885", size = 276593, upload-time = "2026-10-07T12:22:22.12Z" },
240
+ { url = "https://files.pythonhosted.org/packages/bf/72/7619b87dea4261fc27dd7b54c4461c129c1f7d9bb7ba3aec89c797a431b8/tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e", size = 101830, upload-time = "2026-10-07T12:22:23.651Z" },
241
+ { url = "https://files.pythonhosted.org/packages/1e/74/220106da34502304b6751a2a9b8a9fbca6c3fd47e737a2e2e3da7c61c9db/tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8", size = 112742, upload-time = "2026-10-07T12:22:24.972Z" },
242
+ { url = "https://files.pythonhosted.org/packages/27/99/7d9c8b41837a7773613e169504147375c157a290167aa59ad74a085f521f/tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980", size = 109332, upload-time = "2026-10-07T12:22:26.117Z" },
243
+ { url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" },
244
+ { url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" },
245
+ { url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" },
246
+ { url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" },
247
+ { url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" },
248
+ { url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" },
249
+ { url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" },
250
+ { url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" },
251
+ { url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" },
252
+ { url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" },
253
+ { url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" },
254
+ { url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" },
255
+ { url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" },
256
+ { url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" },
257
+ { url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" },
258
+ { url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" },
259
+ { url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" },
260
+ { url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" },
261
+ { url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" },
262
+ { url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" },
263
+ { url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" },
264
+ { url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" },
265
+ { url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" },
266
+ { url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" },
267
+ { url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" },
268
+ { url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" },
269
+ { url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" },
270
+ { url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" },
271
+ { url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" },
272
+ { url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" },
273
+ { url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" },
274
+ { url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" },
275
+ { url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" },
276
+ { url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" },
277
+ { url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" },
278
+ { url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" },
279
+ { url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" },
280
+ { url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" },
281
+ { url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" },
282
+ { url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" },
283
+ { url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" },
284
+ { url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" },
285
+ { url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" },
286
+ { url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" },
287
+ { url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" },
288
+ { url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" },
289
+ { url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" },
290
+ { url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" },
291
+ { url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" },
292
+ { url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" },
293
+ { url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" },
294
+ { url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" },
295
+ { url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" },
296
+ { url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" },
297
+ { url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" },
298
+ ]
299
+
300
+ [[package]]
301
+ name = "typing-extensions"
302
+ version = "4.16.0"
303
+ source = { registry = "https://pypi.org/simple" }
304
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
305
+ wheels = [
306
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
307
+ ]
308
+
309
+ [[package]]
310
+ name = "wheel"
311
+ version = "0.48.0"
312
+ source = { registry = "https://pypi.org/simple" }
313
+ dependencies = [
314
+ { name = "packaging" },
315
+ ]
316
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/20/50ed6bdf27dec98b568a8ae25dc599f35baa3d9709f9e83fd1edb56b9a90/wheel-0.48.0.tar.gz", hash = "sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322", size = 66471, upload-time = "2026-08-11T22:02:27.327Z" }
317
+ wheels = [
318
+ { url = "https://files.pythonhosted.org/packages/2e/29/69cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449/wheel-0.48.0-py3-none-any.whl", hash = "sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab", size = 33320, upload-time = "2026-08-11T22:02:26.1Z" },
319
+ ]
320
+
321
+ [[package]]
322
+ name = "zc-buildout"
323
+ version = "5.2.0"
324
+ source = { registry = "https://pypi.org/simple" }
325
+ resolution-markers = [
326
+ "python_full_version < '3.10'",
327
+ ]
328
+ dependencies = [
329
+ { name = "packaging" },
330
+ { name = "pip", version = "26.0.1", source = { registry = "https://pypi.org/simple" } },
331
+ { name = "setuptools" },
332
+ { name = "wheel" },
333
+ ]
334
+ sdist = { url = "https://files.pythonhosted.org/packages/26/c3/1b32172e7155e60534b48e48f9b507aeb63c69e61bea2a2b9a0421ba5bb0/zc_buildout-5.2.0.tar.gz", hash = "sha256:4dc535723d53a030d463f6efb7b8f0106870a7fa072d4481cf412b9fc95c8396", size = 208594, upload-time = "2026-04-29T07:54:21.786Z" }
335
+ wheels = [
336
+ { url = "https://files.pythonhosted.org/packages/6e/19/273492199083579e8e977db640b386f7e7b3c24b19423d0860594535b5a4/zc_buildout-5.2.0-py3-none-any.whl", hash = "sha256:52ed48feb4a5b52714695ecd78871404b31e386cd1d3f2b2fa6a3ffd93cf2bb2", size = 200527, upload-time = "2026-04-29T07:54:20.105Z" },
337
+ ]
338
+
339
+ [[package]]
340
+ name = "zc-buildout"
341
+ version = "6.0.1"
342
+ source = { registry = "https://pypi.org/simple" }
343
+ resolution-markers = [
344
+ "python_full_version >= '3.11'",
345
+ "python_full_version == '3.10.*'",
346
+ ]
347
+ dependencies = [
348
+ { name = "packaging" },
349
+ { name = "pip", version = "26.2.2", source = { registry = "https://pypi.org/simple" } },
350
+ { name = "platformdirs", version = "4.12.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
351
+ { name = "platformdirs", version = "4.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
352
+ { name = "setuptools" },
353
+ { name = "wheel" },
354
+ ]
355
+ sdist = { url = "https://files.pythonhosted.org/packages/24/20/df73294a2c10b31de291657c1675176390027794e06f138422ea65c26126/zc_buildout-6.0.1.tar.gz", hash = "sha256:b3b0ff2b9e57943dd20f184bcfb554a8177a400ff0c353a41a2369a37fa2c37e", size = 252122, upload-time = "2026-10-15T12:14:37.726Z" }
356
+ wheels = [
357
+ { url = "https://files.pythonhosted.org/packages/e1/c4/bd3c5fa375b16ad9a288b682b32f84f0a879057c11db2366bdd86563a7a9/zc_buildout-6.0.1-py3-none-any.whl", hash = "sha256:e5a9174f3546afed97f66dc0c45d94bfe2bfd313cb48464c957d065cd07e205f", size = 243181, upload-time = "2026-10-15T12:14:36.042Z" },
358
+ ]