nab-python 0.0.1a0__tar.gz → 0.0.3__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.
- nab_python-0.0.3/.gitignore +20 -0
- nab_python-0.0.3/LICENSE +1 -0
- {nab_python-0.0.1a0 → nab_python-0.0.3}/PKG-INFO +4 -4
- nab_python-0.0.3/benchmarks/README.md +53 -0
- nab_python-0.0.3/benchmarks/_profile_runner.py +120 -0
- nab_python-0.0.3/benchmarks/cache/.gitignore +1 -0
- nab_python-0.0.3/benchmarks/canary.py +459 -0
- nab_python-0.0.3/benchmarks/canary_results/.gitignore +2 -0
- nab_python-0.0.3/benchmarks/compare.py +132 -0
- nab_python-0.0.3/benchmarks/results/.gitignore +2 -0
- nab_python-0.0.3/benchmarks/scenarios/ai-stack-lowest-direct.toml +858 -0
- nab_python-0.0.3/benchmarks/scenarios/ai-stack-lowest.toml +858 -0
- nab_python-0.0.3/benchmarks/scenarios/ai-stack.toml +808 -0
- nab_python-0.0.3/benchmarks/scenarios/airflow-lowest-direct.toml +258 -0
- nab_python-0.0.3/benchmarks/scenarios/airflow-lowest.toml +258 -0
- nab_python-0.0.3/benchmarks/scenarios/airflow.toml +237 -0
- nab_python-0.0.3/benchmarks/scenarios/big-packages-lowest-direct.toml +78 -0
- nab_python-0.0.3/benchmarks/scenarios/big-packages-lowest.toml +78 -0
- nab_python-0.0.3/benchmarks/scenarios/big-packages.toml +67 -0
- nab_python-0.0.3/benchmarks/scenarios/cross-tracker-lowest-direct.toml +110 -0
- nab_python-0.0.3/benchmarks/scenarios/cross-tracker-lowest.toml +110 -0
- nab_python-0.0.3/benchmarks/scenarios/cross-tracker.toml +101 -0
- nab_python-0.0.3/benchmarks/scenarios/ecosystem-lowest-direct.toml +531 -0
- nab_python-0.0.3/benchmarks/scenarios/ecosystem-lowest.toml +531 -0
- nab_python-0.0.3/benchmarks/scenarios/ecosystem.toml +490 -0
- nab_python-0.0.3/benchmarks/scenarios/forums-lowest-direct.toml +1349 -0
- nab_python-0.0.3/benchmarks/scenarios/forums-lowest.toml +1349 -0
- nab_python-0.0.3/benchmarks/scenarios/forums.toml +1251 -0
- nab_python-0.0.3/benchmarks/scenarios/pdm-lowest-direct.toml +336 -0
- nab_python-0.0.3/benchmarks/scenarios/pdm-lowest.toml +336 -0
- nab_python-0.0.3/benchmarks/scenarios/pdm.toml +315 -0
- nab_python-0.0.3/benchmarks/scenarios/pip-lowest-direct.toml +2129 -0
- nab_python-0.0.3/benchmarks/scenarios/pip-lowest.toml +2129 -0
- nab_python-0.0.3/benchmarks/scenarios/pip.toml +2024 -0
- nab_python-0.0.3/benchmarks/scenarios/poetry-lowest-direct.toml +545 -0
- nab_python-0.0.3/benchmarks/scenarios/poetry-lowest.toml +545 -0
- nab_python-0.0.3/benchmarks/scenarios/poetry.toml +504 -0
- nab_python-0.0.3/benchmarks/scenarios/pytorch-lowest-direct.toml +175 -0
- nab_python-0.0.3/benchmarks/scenarios/pytorch-lowest.toml +175 -0
- nab_python-0.0.3/benchmarks/scenarios/pytorch.toml +163 -0
- nab_python-0.0.3/benchmarks/scenarios/quick-lowest-direct.toml +13 -0
- nab_python-0.0.3/benchmarks/scenarios/quick-lowest.toml +13 -0
- nab_python-0.0.3/benchmarks/scenarios/quick.toml +11 -0
- nab_python-0.0.3/benchmarks/scenarios/rip-lowest-direct.toml +121 -0
- nab_python-0.0.3/benchmarks/scenarios/rip-lowest.toml +121 -0
- nab_python-0.0.3/benchmarks/scenarios/rip.toml +109 -0
- nab_python-0.0.3/benchmarks/scenarios/universal.toml +335 -0
- nab_python-0.0.3/benchmarks/scenarios/unsupported-lowest-direct.toml +215 -0
- nab_python-0.0.3/benchmarks/scenarios/unsupported-lowest.toml +215 -0
- nab_python-0.0.3/benchmarks/scenarios/unsupported.toml +204 -0
- nab_python-0.0.3/benchmarks/scenarios/uv-lowest-direct.toml +1764 -0
- nab_python-0.0.3/benchmarks/scenarios/uv-lowest.toml +1764 -0
- nab_python-0.0.3/benchmarks/scenarios/uv.toml +1640 -0
- nab_python-0.0.3/benchmarks/scenarios.py +669 -0
- nab_python-0.0.3/benchmarks/strategy_sweep.py +361 -0
- nab_python-0.0.3/benchmarks/strategy_sweep_results/summary.before-build-policy.json +21937 -0
- nab_python-0.0.3/benchmarks/strategy_sweep_results/summary.json +21931 -0
- nab_python-0.0.3/benchmarks/strategy_sweep_results/summary.session28-baseline.json +21970 -0
- nab_python-0.0.3/benchmarks/strategy_sweep_results_fix12/summary.json +21965 -0
- nab_python-0.0.3/benchmarks/strategy_sweep_summary.py +147 -0
- nab_python-0.0.3/benchmarks/universal_scenarios.py +258 -0
- nab_python-0.0.3/benchmarks/universal_summary.py +49 -0
- {nab_python-0.0.1a0 → nab_python-0.0.3}/pyproject.toml +4 -4
- nab_python-0.0.3/src/nab_python/__init__.py +1 -0
- nab_python-0.0.3/src/nab_python/_build/__init__.py +1 -0
- nab_python-0.0.3/src/nab_python/_build/env.py +368 -0
- nab_python-0.0.3/src/nab_python/_build/errors.py +17 -0
- nab_python-0.0.3/src/nab_python/_build/runner.py +254 -0
- nab_python-0.0.3/src/nab_python/_lockfile/__init__.py +1 -0
- nab_python-0.0.3/src/nab_python/_lockfile/builder.py +424 -0
- nab_python-0.0.3/src/nab_python/_lockfile/disjointness.py +207 -0
- nab_python-0.0.3/src/nab_python/_lockfile/pylock.py +412 -0
- nab_python-0.0.3/src/nab_python/_lockfile/requirements.py +121 -0
- nab_python-0.0.3/src/nab_python/_packaging_provider.py +98 -0
- nab_python-0.0.3/src/nab_python/_provider/__init__.py +1 -0
- nab_python-0.0.3/src/nab_python/_provider/build_remote.py +95 -0
- nab_python-0.0.3/src/nab_python/_provider/extras.py +231 -0
- nab_python-0.0.3/src/nab_python/_provider/listing.py +442 -0
- nab_python-0.0.3/src/nab_python/_provider/lookahead.py +156 -0
- nab_python-0.0.3/src/nab_python/_provider/metadata_resolver.py +454 -0
- nab_python-0.0.3/src/nab_python/_provider/priority.py +174 -0
- nab_python-0.0.3/src/nab_python/_provider/sources.py +225 -0
- nab_python-0.0.3/src/nab_python/_testing/__init__.py +1 -0
- nab_python-0.0.3/src/nab_python/_testing/coordinator_fake.py +243 -0
- nab_python-0.0.3/src/nab_python/_vcs_admission.py +185 -0
- nab_python-0.0.3/src/nab_python/_vendor/__init__.py +6 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/LICENSE +3 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/LICENSE.BSD +23 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/PROVENANCE.md +73 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/__init__.py +15 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/_elffile.py +108 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/_manylinux.py +265 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/_musllinux.py +88 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/_parser.py +394 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/_range_utils.py +773 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/_structures.py +33 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/_tokenizer.py +196 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/_version_utils.py +37 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/dependency_groups.py +302 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/direct_url.py +325 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/errors.py +94 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/licenses/__init__.py +186 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/markers.py +506 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/metadata.py +964 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/pylock.py +910 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/ranges.py +1143 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/requirements.py +132 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/specifiers.py +1324 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/tags.py +929 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/utils.py +296 -0
- nab_python-0.0.3/src/nab_python/_vendor/packaging/version.py +1230 -0
- nab_python-0.0.3/src/nab_python/build_backend.py +184 -0
- nab_python-0.0.3/src/nab_python/config.py +845 -0
- nab_python-0.0.3/src/nab_python/download.py +177 -0
- nab_python-0.0.3/src/nab_python/fetch.py +828 -0
- nab_python-0.0.3/src/nab_python/lockfile.py +268 -0
- nab_python-0.0.3/src/nab_python/metadata.py +145 -0
- nab_python-0.0.3/src/nab_python/provider.py +1249 -0
- nab_python-0.0.3/src/nab_python/py.typed +0 -0
- nab_python-0.0.3/src/nab_python/requirements_file.py +209 -0
- nab_python-0.0.3/src/nab_python/resolve.py +512 -0
- nab_python-0.0.3/src/nab_python/universal/__init__.py +1 -0
- nab_python-0.0.3/src/nab_python/universal/matrix.py +235 -0
- nab_python-0.0.3/src/nab_python/universal/provider.py +214 -0
- nab_python-0.0.3/src/nab_python/universal/reresolve.py +310 -0
- nab_python-0.0.3/src/nab_python/universal/resolve.py +550 -0
- nab_python-0.0.3/src/nab_python/universal/validate.py +439 -0
- nab_python-0.0.3/src/nab_python/universal/wheel_selection.py +328 -0
- nab_python-0.0.3/src/nab_python/workspace.py +214 -0
- nab_python-0.0.3/tests/__init__.py +1 -0
- nab_python-0.0.3/tests/property_python/__init__.py +0 -0
- nab_python-0.0.3/tests/property_python/strategies.py +205 -0
- nab_python-0.0.3/tests/property_python/test_extras_pep685.py +288 -0
- nab_python-0.0.3/tests/property_python/test_lockfile_pep751.py +263 -0
- nab_python-0.0.3/tests/property_python/test_marker_overlay.py +135 -0
- nab_python-0.0.3/tests/property_python/test_multi_index_pep503.py +203 -0
- nab_python-0.0.3/tests/property_python/test_resolver_pep440.py +189 -0
- nab_python-0.0.3/tests/ruff.toml +30 -0
- nab_python-0.0.3/tests/test_async_transports.py +378 -0
- nab_python-0.0.3/tests/test_build_backend.py +360 -0
- nab_python-0.0.3/tests/test_build_runner.py +789 -0
- nab_python-0.0.3/tests/test_cache.py +260 -0
- nab_python-0.0.3/tests/test_cached_client.py +687 -0
- nab_python-0.0.3/tests/test_config.py +1216 -0
- nab_python-0.0.3/tests/test_download.py +294 -0
- nab_python-0.0.3/tests/test_fetch.py +1153 -0
- nab_python-0.0.3/tests/test_local_index.py +320 -0
- nab_python-0.0.3/tests/test_lockfile.py +2071 -0
- nab_python-0.0.3/tests/test_metadata.py +72 -0
- nab_python-0.0.3/tests/test_multi_index.py +269 -0
- nab_python-0.0.3/tests/test_provider.py +4581 -0
- nab_python-0.0.3/tests/test_requirements_file.py +320 -0
- nab_python-0.0.3/tests/test_resolve.py +1165 -0
- nab_python-0.0.3/tests/test_resolver_packaging.py +503 -0
- nab_python-0.0.3/tests/test_vcs.py +464 -0
- nab_python-0.0.3/tests/test_vcs_admission.py +283 -0
- nab_python-0.0.3/tests/test_workspace.py +301 -0
- nab_python-0.0.3/tests/universal/__init__.py +0 -0
- nab_python-0.0.3/tests/universal/property_universal/__init__.py +0 -0
- nab_python-0.0.3/tests/universal/property_universal/strategies.py +41 -0
- nab_python-0.0.3/tests/universal/property_universal/test_alignment.py +229 -0
- nab_python-0.0.3/tests/universal/property_universal/test_matrix.py +302 -0
- nab_python-0.0.3/tests/universal/property_universal/test_provider_pep425.py +250 -0
- nab_python-0.0.3/tests/universal/property_universal/test_validate.py +143 -0
- nab_python-0.0.3/tests/universal/test_matrix.py +258 -0
- nab_python-0.0.3/tests/universal/test_reresolve.py +391 -0
- nab_python-0.0.3/tests/universal/test_resolve.py +672 -0
- nab_python-0.0.3/tests/universal/test_universal_provider.py +514 -0
- nab_python-0.0.3/tests/universal/test_validate.py +936 -0
- nab_python-0.0.3/tests/universal/test_wheel_selection.py +371 -0
- nab_python-0.0.1a0/LICENSE +0 -21
- nab_python-0.0.1a0/src/nab_python/__init__.py +0 -3
- {nab_python-0.0.1a0 → nab_python-0.0.3}/README.md +0 -0
- {nab_python-0.0.1a0/src/nab_python → nab_python-0.0.3/src/nab_python/_vendor/packaging}/py.typed +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.hypothesis/
|
|
8
|
+
.coverage*
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
.hatch/
|
|
11
|
+
docs/_build/
|
|
12
|
+
|
|
13
|
+
*token*.txt
|
|
14
|
+
*.pypirc
|
|
15
|
+
.pypirc
|
|
16
|
+
|
|
17
|
+
# Profiling artefacts. By convention these live in `profiling/` at
|
|
18
|
+
# the workspace root and never inside `docs/`, so the Sphinx build
|
|
19
|
+
# never has to scan them.
|
|
20
|
+
profiling/
|
nab_python-0.0.3/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../LICENSE
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nab-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Index-backed provider, lockfile emitter, and downloader for nab
|
|
5
5
|
Project-URL: Homepage, https://github.com/notatallshaw/nab
|
|
6
6
|
Project-URL: Documentation, https://nab.readthedocs.io/
|
|
7
7
|
Project-URL: Issues, https://github.com/notatallshaw/nab/issues
|
|
8
8
|
Project-URL: Source, https://github.com/notatallshaw/nab
|
|
9
|
-
Project-URL: Changelog, https://github.com/notatallshaw/nab/
|
|
9
|
+
Project-URL: Changelog, https://github.com/notatallshaw/nab/releases
|
|
10
10
|
Author-email: Damian Shaw <damian.peter.shaw@gmail.com>
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
@@ -20,8 +20,8 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Requires-Dist: build>=1.2
|
|
22
22
|
Requires-Dist: installer>=0.7
|
|
23
|
-
Requires-Dist: nab-index==0.0.
|
|
24
|
-
Requires-Dist: nab-resolver==0.0.
|
|
23
|
+
Requires-Dist: nab-index==0.0.3
|
|
24
|
+
Requires-Dist: nab-resolver==0.0.3
|
|
25
25
|
Requires-Dist: pyproject-hooks>=1.2
|
|
26
26
|
Requires-Dist: tomli-w>=1.2
|
|
27
27
|
Requires-Dist: tomli>=2.0
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
nab ships two benchmark suites that exercise the single-environment
|
|
4
|
+
and the universal resolver against real-world scenarios.
|
|
5
|
+
|
|
6
|
+
## Single-environment scenarios
|
|
7
|
+
|
|
8
|
+
`nab-python/benchmarks/scenarios.py` runs scenarios drawn from
|
|
9
|
+
real-world resolver issues across pip, uv, poetry, pex, and
|
|
10
|
+
pip-tools. Scenario TOML files live under
|
|
11
|
+
`nab-python/benchmarks/scenarios/`.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python nab-python/benchmarks/scenarios.py
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The runner records wall-clock, decision-count, and round-count
|
|
18
|
+
metrics, and writes a JSON summary under
|
|
19
|
+
`nab-python/benchmarks/results/<commit>/`.
|
|
20
|
+
|
|
21
|
+
## Universal-resolution scenarios
|
|
22
|
+
|
|
23
|
+
`nab-python/benchmarks/universal_scenarios.py` runs
|
|
24
|
+
universal-resolution scenarios sourced from public uv/poetry/pex
|
|
25
|
+
slowness reports. The cases stress cross-tuple alignment, marker
|
|
26
|
+
divergence, and pre-release handling.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python nab-python/benchmarks/universal_scenarios.py
|
|
30
|
+
python nab-python/benchmarks/universal_summary.py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`universal_summary.py` walks the latest results directory and
|
|
34
|
+
prints a markdown table.
|
|
35
|
+
|
|
36
|
+
## Scenario shape
|
|
37
|
+
|
|
38
|
+
Each scenario is a top-level TOML table keyed by name, with at least
|
|
39
|
+
`requirements` and a fixed `datetime` (used as the
|
|
40
|
+
`uploaded-prior-to` cutoff). Optional knobs include constraints,
|
|
41
|
+
marker overlay, dist policy, and build policy.
|
|
42
|
+
|
|
43
|
+
## What the suites cover
|
|
44
|
+
|
|
45
|
+
* Tight version-cluster cases (e.g. boto3, awscli).
|
|
46
|
+
* Conflict graphs that have hit pip's default resolver budget
|
|
47
|
+
(numpy/scipy/scikit-learn matrices).
|
|
48
|
+
* Universal-mode fork-explosion cases (xinference, vllm,
|
|
49
|
+
ultralytics, copick).
|
|
50
|
+
|
|
51
|
+
The suites are diagnostic harnesses that flag regressions on pull
|
|
52
|
+
requests. Walltime is noisy; decision count and round count are
|
|
53
|
+
the load-bearing numbers.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Single-scenario profiling runner.
|
|
2
|
+
|
|
3
|
+
Loads one scenario from the benchmark TOML files and runs the resolver
|
|
4
|
+
once. Designed to be wrapped by ``python -m profiling.sampling run``.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
.venv-3.15/bin/python -m profiling.sampling run -r 5khz \
|
|
8
|
+
--flamegraph -o profile.html \
|
|
9
|
+
nab-python/benchmarks/_profile_runner.py <scenario>
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
if sys.version_info >= (3, 11):
|
|
21
|
+
import tomllib
|
|
22
|
+
else:
|
|
23
|
+
import tomli as tomllib
|
|
24
|
+
|
|
25
|
+
from nab_index.httpx_async_transport import HttpxAsyncTransport
|
|
26
|
+
from nab_index.multi_index import IndexConfig
|
|
27
|
+
from nab_python._vendor.packaging.ranges import VersionRange
|
|
28
|
+
from nab_python._vendor.packaging.requirements import Requirement
|
|
29
|
+
from nab_python._vendor.packaging.utils import canonicalize_name
|
|
30
|
+
from nab_python.fetch import (
|
|
31
|
+
DEFAULT_INDEX_NAME,
|
|
32
|
+
DEFAULT_INDEX_URL,
|
|
33
|
+
FetchCoordinator,
|
|
34
|
+
)
|
|
35
|
+
from nab_python.provider import DistPolicy, Provider
|
|
36
|
+
from nab_resolver.resolver import Resolver
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from collections.abc import Iterable
|
|
40
|
+
|
|
41
|
+
BENCHMARKS_DIR = Path(__file__).parent
|
|
42
|
+
SCENARIOS_DIR = BENCHMARKS_DIR / "scenarios"
|
|
43
|
+
CACHE_DIR = BENCHMARKS_DIR / "cache"
|
|
44
|
+
DEFAULT_INDEXES = (IndexConfig(DEFAULT_INDEX_NAME, DEFAULT_INDEX_URL),)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_requirements(strs: Iterable[str]) -> dict[str, VersionRange]:
|
|
48
|
+
out: dict[str, VersionRange] = {}
|
|
49
|
+
for r in strs:
|
|
50
|
+
req = Requirement(r)
|
|
51
|
+
name = canonicalize_name(req.name)
|
|
52
|
+
vi = req.specifier.to_range()
|
|
53
|
+
if vi is not None:
|
|
54
|
+
out[name] = vi
|
|
55
|
+
for ex in req.extras:
|
|
56
|
+
out[f"{name}[{ex}]"] = VersionRange.full()
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def find_scenario(name: str) -> dict[str, Any] | None:
|
|
61
|
+
for p in SCENARIOS_DIR.glob("*.toml"):
|
|
62
|
+
with p.open("rb") as f:
|
|
63
|
+
data = tomllib.load(f)
|
|
64
|
+
if name in data:
|
|
65
|
+
return data[name]
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main() -> None:
|
|
70
|
+
name = sys.argv[1]
|
|
71
|
+
scn = find_scenario(name)
|
|
72
|
+
if scn is None:
|
|
73
|
+
print(f"scenario {name!r} not found", file=sys.stderr)
|
|
74
|
+
sys.exit(2)
|
|
75
|
+
reqs = parse_requirements(scn["requirements"])
|
|
76
|
+
constraints = (
|
|
77
|
+
parse_requirements(scn.get("constraints", []))
|
|
78
|
+
if scn.get("constraints")
|
|
79
|
+
else None
|
|
80
|
+
)
|
|
81
|
+
py = scn["python_version"]
|
|
82
|
+
dt = scn.get("datetime")
|
|
83
|
+
upload = datetime.fromisoformat(dt).replace(tzinfo=timezone.utc) if dt else None
|
|
84
|
+
with FetchCoordinator(
|
|
85
|
+
HttpxAsyncTransport(),
|
|
86
|
+
indexes=list(DEFAULT_INDEXES),
|
|
87
|
+
cache_dir=CACHE_DIR,
|
|
88
|
+
) as coord:
|
|
89
|
+
provider = Provider(
|
|
90
|
+
coord,
|
|
91
|
+
python_version=py,
|
|
92
|
+
root_requirements=reqs,
|
|
93
|
+
uploaded_prior_to=upload,
|
|
94
|
+
dist_policy=DistPolicy.PREFER_BINARY,
|
|
95
|
+
)
|
|
96
|
+
resolver = Resolver(
|
|
97
|
+
provider,
|
|
98
|
+
range_type=VersionRange,
|
|
99
|
+
root_version="0",
|
|
100
|
+
max_iterations=200_000,
|
|
101
|
+
)
|
|
102
|
+
t0 = time.monotonic()
|
|
103
|
+
try:
|
|
104
|
+
resolver.resolve(reqs, constraints=constraints)
|
|
105
|
+
elapsed = time.monotonic() - t0
|
|
106
|
+
print(
|
|
107
|
+
f"{name}: OK in {elapsed:.2f}s, "
|
|
108
|
+
f"{resolver.stats.decisions} decisions, "
|
|
109
|
+
f"{resolver.stats.conflicts} conflicts"
|
|
110
|
+
)
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
elapsed = time.monotonic() - t0
|
|
113
|
+
print(
|
|
114
|
+
f"{name}: FAILED ({type(exc).__name__}) in {elapsed:.2f}s, "
|
|
115
|
+
f"{resolver.stats.decisions} decisions"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Quick canary benchmark for fast iteration.
|
|
2
|
+
|
|
3
|
+
Runs a curated subset of scenarios (canaries + hard cases) N times each
|
|
4
|
+
and reports median decisions, conflicts, and wall time. The set is
|
|
5
|
+
small enough to finish in a few minutes so it can be re-run after each
|
|
6
|
+
algorithm change.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python nab-python/benchmarks/canary.py [--commit LABEL] [--runs N]
|
|
10
|
+
[--scenario NAME] [--scenarios FILE]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import signal
|
|
18
|
+
import statistics
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Mapping
|
|
28
|
+
|
|
29
|
+
if sys.version_info >= (3, 11):
|
|
30
|
+
import tomllib
|
|
31
|
+
else:
|
|
32
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
33
|
+
|
|
34
|
+
from nab_index.httpx_async_transport import HttpxAsyncTransport
|
|
35
|
+
from nab_index.multi_index import IndexConfig
|
|
36
|
+
from nab_python._vcs_admission import admit_vcs_url
|
|
37
|
+
from nab_python._vendor.packaging.markers import default_environment
|
|
38
|
+
from nab_python._vendor.packaging.ranges import VersionRange
|
|
39
|
+
from nab_python._vendor.packaging.requirements import Requirement
|
|
40
|
+
from nab_python._vendor.packaging.utils import canonicalize_name
|
|
41
|
+
from nab_python.fetch import (
|
|
42
|
+
DEFAULT_INDEX_NAME,
|
|
43
|
+
DEFAULT_INDEX_URL,
|
|
44
|
+
FetchCoordinator,
|
|
45
|
+
IndexOverride,
|
|
46
|
+
)
|
|
47
|
+
from nab_python.provider import (
|
|
48
|
+
BuildPolicy,
|
|
49
|
+
DistPolicy,
|
|
50
|
+
Provider,
|
|
51
|
+
VcsConfig,
|
|
52
|
+
VcsPolicy,
|
|
53
|
+
split_extra,
|
|
54
|
+
)
|
|
55
|
+
from nab_resolver.resolver import Resolver
|
|
56
|
+
|
|
57
|
+
BENCHMARKS_DIR = Path(__file__).parent
|
|
58
|
+
SCENARIOS_DIR = BENCHMARKS_DIR / "scenarios"
|
|
59
|
+
RESULTS_DIR = BENCHMARKS_DIR / "canary_results"
|
|
60
|
+
CACHE_DIR = BENCHMARKS_DIR / "cache"
|
|
61
|
+
DEFAULT_INDEXES: tuple[IndexConfig, ...] = (
|
|
62
|
+
IndexConfig(DEFAULT_INDEX_NAME, DEFAULT_INDEX_URL),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
CANARY_SCENARIOS = [
|
|
66
|
+
"boto3-urllib3-transient",
|
|
67
|
+
"trustllm",
|
|
68
|
+
"copick",
|
|
69
|
+
"promptflow-vectordb",
|
|
70
|
+
"ultralytics-export",
|
|
71
|
+
"datacontract-cli",
|
|
72
|
+
"pandas-aws-boto3-dandi-frenzy",
|
|
73
|
+
"vllm-transformers-floor",
|
|
74
|
+
"google-bigquery-soda",
|
|
75
|
+
"langchain-ml-course",
|
|
76
|
+
"airflow-3-0-2-awswrangler",
|
|
77
|
+
"airflow-3-0-3-pandas-sqlalchemy",
|
|
78
|
+
"airflow-portalocker-qdrant",
|
|
79
|
+
"airflow-fastapi-121",
|
|
80
|
+
"so-dbt-core-snowflake-79744735",
|
|
81
|
+
"uv-issue-16601-xinference",
|
|
82
|
+
"uv-issue-16601-xinference-fixed",
|
|
83
|
+
"rag-chroma-langchain",
|
|
84
|
+
"streamlit-langchain",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
WALL_TIMEOUT_S = 60
|
|
88
|
+
MAX_ITERATIONS = 50_000
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _ScenarioTimeoutError(Exception):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _alarm_handler(_signum: int, _frame: object) -> None:
|
|
96
|
+
msg = f"scenario exceeded {WALL_TIMEOUT_S}s wall-clock budget"
|
|
97
|
+
raise _ScenarioTimeoutError(msg)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def expand_project_extras(
|
|
101
|
+
project_name: str,
|
|
102
|
+
requested_extras: list[str],
|
|
103
|
+
optional_dependencies: dict[str, list[str]],
|
|
104
|
+
) -> list[str]:
|
|
105
|
+
canonical_project = canonicalize_name(project_name)
|
|
106
|
+
norm_optional = {canonicalize_name(k): v for k, v in optional_dependencies.items()}
|
|
107
|
+
visited: set[str] = set()
|
|
108
|
+
out: list[str] = []
|
|
109
|
+
|
|
110
|
+
def visit(extra: str) -> None:
|
|
111
|
+
norm = canonicalize_name(extra)
|
|
112
|
+
if norm in visited:
|
|
113
|
+
return
|
|
114
|
+
visited.add(norm)
|
|
115
|
+
for dep_str in norm_optional.get(norm, []):
|
|
116
|
+
req = Requirement(dep_str)
|
|
117
|
+
if canonicalize_name(req.name) == canonical_project:
|
|
118
|
+
for sub_extra in sorted(req.extras):
|
|
119
|
+
visit(sub_extra)
|
|
120
|
+
else:
|
|
121
|
+
out.append(dep_str)
|
|
122
|
+
|
|
123
|
+
for extra in requested_extras:
|
|
124
|
+
visit(extra)
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def parse_requirements(
|
|
129
|
+
requirement_strings: list[str],
|
|
130
|
+
*,
|
|
131
|
+
vcs_config: VcsConfig | None = None,
|
|
132
|
+
marker_environment: dict[str, str] | None = None,
|
|
133
|
+
) -> dict[str, VersionRange]:
|
|
134
|
+
config = vcs_config or VcsConfig()
|
|
135
|
+
env = _full_marker_environment(marker_environment)
|
|
136
|
+
reqs: dict[str, VersionRange] = {}
|
|
137
|
+
for req_str in requirement_strings:
|
|
138
|
+
req = Requirement(req_str)
|
|
139
|
+
if req.marker is not None and not req.marker.evaluate(env):
|
|
140
|
+
continue
|
|
141
|
+
if req.url is not None:
|
|
142
|
+
admit_vcs_url(req.url, config)
|
|
143
|
+
msg = (
|
|
144
|
+
f"VCS requirement admitted by policy but resolver path is not"
|
|
145
|
+
f" yet implemented: {req.name} @ {req.url}"
|
|
146
|
+
)
|
|
147
|
+
raise NotImplementedError(msg)
|
|
148
|
+
name = canonicalize_name(req.name)
|
|
149
|
+
reqs[name] = req.specifier.to_range()
|
|
150
|
+
for extra in req.extras:
|
|
151
|
+
reqs[f"{name}[{extra}]"] = VersionRange.full()
|
|
152
|
+
return reqs
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _full_marker_environment(
|
|
156
|
+
overlay: dict[str, str] | None,
|
|
157
|
+
) -> dict[str, str]:
|
|
158
|
+
env = dict(default_environment())
|
|
159
|
+
if overlay:
|
|
160
|
+
env.update(overlay)
|
|
161
|
+
return env
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
_PYTHON_VERSION_PARTS = 2
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _scenario_marker_env(
|
|
168
|
+
python_version: str,
|
|
169
|
+
overlay: dict[str, str],
|
|
170
|
+
) -> dict[str, str]:
|
|
171
|
+
env = dict(overlay)
|
|
172
|
+
env.setdefault("python_full_version", python_version)
|
|
173
|
+
if "python_version" not in env:
|
|
174
|
+
parts = python_version.split(".")
|
|
175
|
+
env["python_version"] = (
|
|
176
|
+
".".join(parts[:_PYTHON_VERSION_PARTS])
|
|
177
|
+
if len(parts) >= _PYTHON_VERSION_PARTS
|
|
178
|
+
else python_version
|
|
179
|
+
)
|
|
180
|
+
return env
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def parse_datetime(value: str) -> datetime:
|
|
184
|
+
dt = datetime.fromisoformat(value)
|
|
185
|
+
if dt.tzinfo is None:
|
|
186
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
187
|
+
return dt
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_git_commit() -> str:
|
|
191
|
+
result = subprocess.run(
|
|
192
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
193
|
+
capture_output=True,
|
|
194
|
+
text=True,
|
|
195
|
+
check=True,
|
|
196
|
+
)
|
|
197
|
+
return result.stdout.strip()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def run_one(
|
|
201
|
+
requirements: dict[str, VersionRange],
|
|
202
|
+
python_version: str,
|
|
203
|
+
uploaded_prior_to: datetime | None,
|
|
204
|
+
constraints: dict[str, VersionRange] | None,
|
|
205
|
+
marker_environment: dict[str, str] | None = None,
|
|
206
|
+
indexes: list[IndexConfig] | None = None,
|
|
207
|
+
index_overrides: list[IndexOverride] | None = None,
|
|
208
|
+
build_policy_overrides: Mapping[str, BuildPolicy] | None = None,
|
|
209
|
+
) -> dict:
|
|
210
|
+
with FetchCoordinator(
|
|
211
|
+
HttpxAsyncTransport(),
|
|
212
|
+
indexes=indexes,
|
|
213
|
+
cache_dir=CACHE_DIR,
|
|
214
|
+
index_overrides=index_overrides,
|
|
215
|
+
marker_environment=marker_environment,
|
|
216
|
+
) as coordinator:
|
|
217
|
+
provider = Provider(
|
|
218
|
+
coordinator,
|
|
219
|
+
python_version=python_version,
|
|
220
|
+
root_requirements=requirements,
|
|
221
|
+
uploaded_prior_to=uploaded_prior_to,
|
|
222
|
+
dist_policy=DistPolicy.WHEEL_OR_SDIST,
|
|
223
|
+
build_policy=BuildPolicy.NEVER,
|
|
224
|
+
build_policy_overrides=build_policy_overrides,
|
|
225
|
+
marker_environment=marker_environment,
|
|
226
|
+
)
|
|
227
|
+
resolver = Resolver(
|
|
228
|
+
provider,
|
|
229
|
+
range_type=VersionRange,
|
|
230
|
+
root_version="0",
|
|
231
|
+
max_iterations=MAX_ITERATIONS,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
previous_handler = signal.signal(signal.SIGALRM, _alarm_handler)
|
|
235
|
+
signal.alarm(WALL_TIMEOUT_S)
|
|
236
|
+
start = time.monotonic()
|
|
237
|
+
try:
|
|
238
|
+
raw = resolver.resolve(requirements, constraints=constraints)
|
|
239
|
+
elapsed = time.monotonic() - start
|
|
240
|
+
result = {k: v for k, v in raw.items() if split_extra(k)[1] is None}
|
|
241
|
+
success = True
|
|
242
|
+
error = None
|
|
243
|
+
packages = len(result)
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
elapsed = time.monotonic() - start
|
|
246
|
+
success = False
|
|
247
|
+
error = f"{type(exc).__name__}: {exc}"
|
|
248
|
+
packages = 0
|
|
249
|
+
finally:
|
|
250
|
+
signal.alarm(0)
|
|
251
|
+
signal.signal(signal.SIGALRM, previous_handler)
|
|
252
|
+
|
|
253
|
+
rs = resolver.stats
|
|
254
|
+
ps = provider.stats
|
|
255
|
+
return {
|
|
256
|
+
"success": success,
|
|
257
|
+
"error": error,
|
|
258
|
+
"decisions": rs.decisions,
|
|
259
|
+
"conflicts": rs.conflicts,
|
|
260
|
+
"backjumps": rs.backjumps,
|
|
261
|
+
"restarts": rs.restarts,
|
|
262
|
+
"incompatibilities_learned": rs.incompatibilities_learned,
|
|
263
|
+
"metadata_fetched": ps.metadata_fetched,
|
|
264
|
+
"look_ahead_rejections": ps.look_ahead_rejections,
|
|
265
|
+
"packages": packages,
|
|
266
|
+
"wall_time_seconds": round(elapsed, 3),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def find_scenario(scenario_name: str) -> dict | None:
|
|
271
|
+
for toml_file in SCENARIOS_DIR.glob("*.toml"):
|
|
272
|
+
with toml_file.open("rb") as f:
|
|
273
|
+
data = tomllib.load(f)
|
|
274
|
+
if scenario_name in data:
|
|
275
|
+
return data[scenario_name]
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def median_run(scenario: dict, runs: int) -> tuple[list[dict], dict]:
|
|
280
|
+
if "unsupported_reason" in scenario:
|
|
281
|
+
return [], {"skipped": scenario["unsupported_reason"]}
|
|
282
|
+
|
|
283
|
+
python_version = scenario["python_version"]
|
|
284
|
+
requirement_strings = scenario["requirements"]
|
|
285
|
+
constraint_strings = scenario.get("constraints", [])
|
|
286
|
+
platform_system = scenario.get("platform_system")
|
|
287
|
+
marker_environment_raw = scenario.get("marker_environment", {})
|
|
288
|
+
if not isinstance(marker_environment_raw, dict):
|
|
289
|
+
msg = (
|
|
290
|
+
"marker_environment must be a TOML table of string -> string,"
|
|
291
|
+
f" got {type(marker_environment_raw).__name__}"
|
|
292
|
+
)
|
|
293
|
+
raise TypeError(msg)
|
|
294
|
+
marker_environment: dict[str, str] = {
|
|
295
|
+
str(k): str(v) for k, v in marker_environment_raw.items()
|
|
296
|
+
}
|
|
297
|
+
if platform_system and "platform_system" not in marker_environment:
|
|
298
|
+
marker_environment["platform_system"] = platform_system
|
|
299
|
+
datetime_str = scenario.get("datetime")
|
|
300
|
+
project_name = scenario.get("project_name")
|
|
301
|
+
project_extras = scenario.get("project_extras", [])
|
|
302
|
+
optional_dependencies = scenario.get("optional_dependencies", {})
|
|
303
|
+
vcs_config = VcsConfig(
|
|
304
|
+
policy=VcsPolicy(scenario.get("vcs_policy", "block")),
|
|
305
|
+
allowed_schemes=frozenset(scenario.get("vcs_allowed_schemes", [])),
|
|
306
|
+
allowed_repos=tuple(scenario.get("vcs_allowed_repos", [])),
|
|
307
|
+
require_pin=scenario.get("vcs_require_pin", True),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if project_name:
|
|
311
|
+
requirement_strings = [
|
|
312
|
+
*requirement_strings,
|
|
313
|
+
*expand_project_extras(project_name, project_extras, optional_dependencies),
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
raw_indexes = scenario.get("indexes")
|
|
317
|
+
if raw_indexes is None:
|
|
318
|
+
indexes = list(DEFAULT_INDEXES)
|
|
319
|
+
else:
|
|
320
|
+
indexes = [
|
|
321
|
+
IndexConfig(name=str(entry["name"]), url=str(entry["url"]))
|
|
322
|
+
for entry in raw_indexes
|
|
323
|
+
]
|
|
324
|
+
raw_overrides = scenario.get("index_overrides", [])
|
|
325
|
+
index_overrides = [
|
|
326
|
+
IndexOverride(
|
|
327
|
+
name=str(entry["name"]),
|
|
328
|
+
index=str(entry["index"]),
|
|
329
|
+
marker=entry.get("marker"),
|
|
330
|
+
)
|
|
331
|
+
for entry in raw_overrides
|
|
332
|
+
]
|
|
333
|
+
raw_build_packages = scenario.get("build_packages", []) or []
|
|
334
|
+
build_policy_overrides = {
|
|
335
|
+
str(name): BuildPolicy.BUILD_REMOTE for name in raw_build_packages
|
|
336
|
+
}
|
|
337
|
+
if marker_environment and build_policy_overrides:
|
|
338
|
+
# See ``scenarios.py``: marker_environment + BUILD_REMOTE override
|
|
339
|
+
# is rejected to preserve metadata soundness. Drop the overrides;
|
|
340
|
+
# a resolution that now fails was relying on the previous silent
|
|
341
|
+
# passthrough and needs an audit.
|
|
342
|
+
print(
|
|
343
|
+
f" [audit] dropping {len(build_policy_overrides)} build_packages"
|
|
344
|
+
" override(s) because of marker_environment overlay.",
|
|
345
|
+
flush=True,
|
|
346
|
+
)
|
|
347
|
+
build_policy_overrides = {}
|
|
348
|
+
requirement_marker_env = _scenario_marker_env(python_version, marker_environment)
|
|
349
|
+
requirements = parse_requirements(
|
|
350
|
+
requirement_strings,
|
|
351
|
+
vcs_config=vcs_config,
|
|
352
|
+
marker_environment=requirement_marker_env,
|
|
353
|
+
)
|
|
354
|
+
constraints = (
|
|
355
|
+
parse_requirements(
|
|
356
|
+
constraint_strings,
|
|
357
|
+
vcs_config=vcs_config,
|
|
358
|
+
marker_environment=requirement_marker_env,
|
|
359
|
+
)
|
|
360
|
+
if constraint_strings
|
|
361
|
+
else None
|
|
362
|
+
)
|
|
363
|
+
uploaded_prior_to = parse_datetime(datetime_str) if datetime_str else None
|
|
364
|
+
|
|
365
|
+
runs_data: list[dict] = [
|
|
366
|
+
run_one(
|
|
367
|
+
requirements,
|
|
368
|
+
python_version,
|
|
369
|
+
uploaded_prior_to,
|
|
370
|
+
constraints,
|
|
371
|
+
marker_environment=marker_environment or None,
|
|
372
|
+
indexes=indexes,
|
|
373
|
+
index_overrides=index_overrides or None,
|
|
374
|
+
build_policy_overrides=build_policy_overrides or None,
|
|
375
|
+
)
|
|
376
|
+
for _ in range(runs)
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
def med(key: str) -> float:
|
|
380
|
+
vals = [r[key] for r in runs_data if isinstance(r.get(key), (int, float))]
|
|
381
|
+
return statistics.median(vals) if vals else 0
|
|
382
|
+
|
|
383
|
+
successes = sum(1 for r in runs_data if r["success"])
|
|
384
|
+
summary = {
|
|
385
|
+
"success_runs": f"{successes}/{len(runs_data)}",
|
|
386
|
+
"median_decisions": int(med("decisions")),
|
|
387
|
+
"median_conflicts": int(med("conflicts")),
|
|
388
|
+
"median_backjumps": int(med("backjumps")),
|
|
389
|
+
"median_wall": round(med("wall_time_seconds"), 2),
|
|
390
|
+
"min_decisions": min(r["decisions"] for r in runs_data),
|
|
391
|
+
"max_decisions": max(r["decisions"] for r in runs_data),
|
|
392
|
+
"min_wall": round(min(r["wall_time_seconds"] for r in runs_data), 2),
|
|
393
|
+
"max_wall": round(max(r["wall_time_seconds"] for r in runs_data), 2),
|
|
394
|
+
}
|
|
395
|
+
return runs_data, summary
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def main() -> None:
|
|
399
|
+
parser = argparse.ArgumentParser(description="Run canary benchmark")
|
|
400
|
+
parser.add_argument("--commit", default=None)
|
|
401
|
+
parser.add_argument("--runs", type=int, default=3)
|
|
402
|
+
parser.add_argument("--scenario", action="append", help="Run only named scenarios")
|
|
403
|
+
parser.add_argument("--scenarios-list", help="File with one scenario per line")
|
|
404
|
+
args = parser.parse_args()
|
|
405
|
+
|
|
406
|
+
commit = args.commit or get_git_commit()
|
|
407
|
+
|
|
408
|
+
scenarios_to_run: list[str]
|
|
409
|
+
if args.scenarios_list:
|
|
410
|
+
with Path(args.scenarios_list).open(encoding="utf-8") as f:
|
|
411
|
+
scenarios_to_run = [line.strip() for line in f if line.strip()]
|
|
412
|
+
elif args.scenario:
|
|
413
|
+
scenarios_to_run = args.scenario
|
|
414
|
+
else:
|
|
415
|
+
scenarios_to_run = list(CANARY_SCENARIOS)
|
|
416
|
+
|
|
417
|
+
out_dir = RESULTS_DIR / commit
|
|
418
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
419
|
+
|
|
420
|
+
print(f"\n=== Canary benchmark, commit={commit}, runs={args.runs} ===")
|
|
421
|
+
print(
|
|
422
|
+
f"{'scenario':<45} "
|
|
423
|
+
f"{'success':>9} "
|
|
424
|
+
f"{'med_dec':>8} "
|
|
425
|
+
f"{'med_wall':>10} "
|
|
426
|
+
f"{'min_dec':>8} "
|
|
427
|
+
f"{'max_dec':>8}"
|
|
428
|
+
)
|
|
429
|
+
print("-" * 100)
|
|
430
|
+
|
|
431
|
+
summary_all: dict[str, dict] = {}
|
|
432
|
+
for name in scenarios_to_run:
|
|
433
|
+
scenario = find_scenario(name)
|
|
434
|
+
if scenario is None:
|
|
435
|
+
print(f"{name:<45} NOT FOUND")
|
|
436
|
+
continue
|
|
437
|
+
runs_data, summary = median_run(scenario, args.runs)
|
|
438
|
+
summary_all[name] = {"runs": runs_data, "summary": summary}
|
|
439
|
+
|
|
440
|
+
if "skipped" in summary:
|
|
441
|
+
print(f"{name:<45} SKIPPED: {summary['skipped']}")
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
print(
|
|
445
|
+
f"{name:<45} "
|
|
446
|
+
f"{summary['success_runs']:>9} "
|
|
447
|
+
f"{summary['median_decisions']:>8} "
|
|
448
|
+
f"{summary['median_wall']:>10} "
|
|
449
|
+
f"{summary['min_decisions']:>8} "
|
|
450
|
+
f"{summary['max_decisions']:>8}"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
out_file = out_dir / f"canary_{int(time.time())}.json"
|
|
454
|
+
out_file.write_text(json.dumps(summary_all, indent=2) + "\n")
|
|
455
|
+
print(f"\nResults: {out_file}")
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
main()
|