diff --git a/.gitignore b/.gitignore index 241169a8..840530e5 100755 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ CLAUDE.md .roomodes LOCAL_BRANCH_NOTES.md .planning/ +.gsd-tmp/ # DLIO test artifacts — created in cwd when running dlio_benchmark tests output/ @@ -70,6 +71,12 @@ env-fast sim_*.tsv sim_*.tsv.zst +# Submission checker default CSV output (utility_args.py default --csv path, +# README.md §submission-checker). Runtime artifact, not source — keep +# untracked so contributors don't see a dirty working tree after running +# `mlpstorage validate ...` without an explicit --csv path. +summary.csv + # Sweep run logs and results (local benchmark output) sweep_logs/ sweep_flux_master.log diff --git a/Rules.md b/Rules.md index b4374adf..127e5eb3 100644 --- a/Rules.md +++ b/Rules.md @@ -65,11 +65,31 @@ The `mlpstorage` tool must be used to run the benchmarks, submitters are not all 2.1.4. **closedSubmitterDirectory** -- Within the "closed" directory, each submitter's contribution lives in a directory whose name is the submitter's name (subject to 2.1.1). Reviewers may run the submission checker against either a single submitter's pre-merge package (in which case the "closed" directory contains exactly one submitter directory, whose name matches the top-level submitter directory) or a merged tree containing multiple submitters' packages (in which case the "closed" directory contains one directory per participating submitter and the top-level directory is named for the merged set rather than any one submitter). The same convention applies to the "open" directory per 2.1.3. -2.1.5. **requiredSubdirectories** -- Within the submitter directory mentioned just above, there must be exactly three directories: "code", "results", and "systems". These names are case-sensitive. +2.1.5. **requiredSubdirectories** -- The required subdirectories at the submitter level differ between CLOSED and OPEN submissions: -2.1.6. **codeDirectoryContents** -- The "code" directory must include a complete copy of the MLPerf Storage github repo that was used to run the test that resulted in the "results" directory's contents. -If this is in the "open" hierarchy, any modifications made to the benchmark code must be included here, and if this is in the "closed" hierarchy, there must be no changes to the benchmark code. -Note that in both cases this must be the code that was actually run to generate those results. In a CLOSED submission, the *submission validator* should do an md5sum of the code directory hierarchy, compare that to a value hard-coded into the validator code, and fail the validation if there is a difference. +2.1.5.a. **requiredSubdirectoriesClosed** -- Within a CLOSED submitter directory, there must be exactly three directories: "code", "results", and "systems". These names are case-sensitive. + +2.1.5.b. **requiredSubdirectoriesOpen** -- Within an OPEN submitter directory, there must be exactly two directories: "results" and "systems". These names are case-sensitive. The "code" directory does NOT appear at the OPEN submitter level; instead, a "code" directory is captured at each leaf inside `results/`. The leaf shape is per-benchmark-type: +- For "training" and "checkpointing" the leaf is `results////` (one capture per model). +- For "vector_database" the leaf is `results//vector_database//` where `` is the UPPERCASE token (`DISKANN`, `HNSW`, or `AISAQ`) (one capture per index type, because results across index types — e.g. AISAQ vs DISKANN vs HNSW — are not comparable and must live in separate trees). +- For "kv_cache" the leaf is currently `results///` (one capture per type). This is transitional pending finalization of the kv_cache directory structure below the type prefix. + +See §2.1.6 and §2.1.27. + +2.1.6. **codeDirectoryContents** -- Each "code" directory in the submission package must be a captured copy of the MLPerf Storage source tree that was used to generate the corresponding results, accompanied by a top-level ".code-hash.json" file that records the captured tree's hash and metadata. + +The "code" directory is created automatically by the `mlpstorage` CLI on the first invocation of `closed|open datasize|datagen|run`. On subsequent invocations, the CLI verifies that the live source tree matches the recorded hash and refuses to proceed on mismatch (with the exact message "changes to the codebase are not allowed in a CLOSED run" for CLOSED, or "all runs of this type must use the same codebase" for OPEN). See §2.1.27 for the per-leaf location of "code" in OPEN submissions. + +The ".code-hash.json" schema is: +- "hash": 32-character lowercase hex MD5 of the captured tree (excluding dotfiles, dotdirs, `test/`, `tests/`, `__pycache__/`, `.egg-info/`, `*.pyc`, and `.code-hash.json` itself). +- "algorithm": stable identifier (currently "md5-tree-v1"). +- "captured_at": ISO-8601 UTC timestamp of the capture (e.g., "2026-06-16T15:42:11Z"). +- "mlpstorage_version": the `mlpstorage` package version at capture time. +- "git_sha": full 40-character SHA of HEAD at capture, or null if unavailable. + +At submission-validation time, the *submission validator* performs a per-tree self-consistency check on every "code" directory it finds: it recomputes the captured tree's MD5 (using the same exclusion set above) and compares it against the recorded "hash" in ".code-hash.json". Mismatch produces a violation under §2.1.6. + +For CLOSED submissions, an additional upstream-identity check is layered on top: the validator compares the captured tree's MD5 against a pinned digest from `REFERENCE_CHECKSUMS` (or a value supplied via `--reference-checksum`). See §3.6.1. 2.1.7. **systemsDirectoryFiles** -- The "systems" directory must contain two files for each "system name", a .yaml file and a .pdf file, and nothing more, with two exceptions: Markdown files (any "*.md", e.g. "README.md", "NOTES.md") are permitted alongside the per-system files so submitters may include supplementary documentation, and dot-prefixed entries (such as ".DS_Store" or ".gitkeep") are ignored. Each of the .yaml/.pdf files must be named with the "system name". Eg: for a system-under-test named "Big_and_Fast_4000_buffered", there must be a "Big_and_Fast_4000_buffered.yaml" and a "Big_and_Fast_4000_buffered.pdf" file. These names are case-sensitive. @@ -174,25 +194,37 @@ root_folder (or any name you prefer) │ │ │ ... (10x Runs for Read and Write. May be combined in a single run) │ │ │ └── YYYYMMDD_HHmmss │ │ │ └── dlio_config -│ │ └── vdb_bench -| | ├── AiSAQ -│ │ | ├── YYYYMMDD_HHmmss -│ │ | │ └── summary.json -│ │ | ... (5x Runs total) -│ │ | └── YYYYMMDD_HHmmss -│ │ | └── summary.json -| | ├── DiskANN -│ │ | ├── YYYYMMDD_HHmmss -│ │ | │ └── summary.json -│ │ | ... (5x Runs total) -│ │ | └── YYYYMMDD_HHmmss -│ │ | └── summary.json +│ │ └── vector_database +| | ├── AISAQ +│ │ | ├── datagen +│ │ | │ └── YYYYMMDD_HHmmss +│ │ | │ └── summary.json +│ │ | └── run +│ │ | ├── YYYYMMDD_HHmmss +│ │ | │ └── summary.json +│ │ | ... (5x Runs total) +│ │ | └── YYYYMMDD_HHmmss +│ │ | └── summary.json +| | ├── DISKANN +│ │ | ├── datagen +│ │ | │ └── YYYYMMDD_HHmmss +│ │ | │ └── summary.json +│ │ | └── run +│ │ | ├── YYYYMMDD_HHmmss +│ │ | │ └── summary.json +│ │ | ... (5x Runs total) +│ │ | └── YYYYMMDD_HHmmss +│ │ | └── summary.json | | └── HNSW -│ │ ├── YYYYMMDD_HHmmss -│ │ │ └── summary.json -│ │ ... (5x Runs total) -│ │ └── YYYYMMDD_HHmmss -│ │ └── summary.json +│ │ ├── datagen +│ │ │ └── YYYYMMDD_HHmmss +│ │ │ └── summary.json +│ │ └── run +│ │ ├── YYYYMMDD_HHmmss +│ │ │ └── summary.json +│ │ ... (5x Runs total) +│ │ └── YYYYMMDD_HHmmss +│ │ └── summary.json │ └── systems │ ├──system-name-1.yaml │ ├──system-name-1.pdf @@ -201,11 +233,11 @@ root_folder (or any name you prefer) │ └── Open └── - ├── code ├── results │ └──system-name-1 │ ├── training │ │ ├── unet3d + │ │ │ ├── code # captured per-leaf │ │ │ ├── datagen │ │ │ │ └── YYYYMMDD_HHmmss │ │ │ │ └── dlio_config @@ -229,6 +261,7 @@ root_folder (or any name you prefer) │ │ └── dlio_config │ ├── checkpointing │ │ ├── llama3-8b + │ │ │ ├── code # captured per-leaf │ │ │ ├──results.json │ │ │ ├── YYYYMMDD_HHmmss │ │ │ │ └── dlio_config @@ -236,6 +269,7 @@ root_folder (or any name you prefer) │ │ │ └── YYYYMMDD_HHmmss │ │ │ └── dlio_config │ │ ├── llama3-70b + │ │ │ ├── code # captured per-leaf │ │ │ ├──results.json │ │ │ ├── YYYYMMDD_HHmmss │ │ │ │ └── dlio_config @@ -243,6 +277,7 @@ root_folder (or any name you prefer) │ │ │ └── YYYYMMDD_HHmmss │ │ │ └── dlio_config │ │ ├── llama3-405b + │ │ │ ├── code # captured per-leaf │ │ │ ├──results.json │ │ │ ├── YYYYMMDD_HHmmss │ │ │ │ └── dlio_config @@ -250,31 +285,47 @@ root_folder (or any name you prefer) │ │ │ └── YYYYMMDD_HHmmss │ │ │ └── dlio_config │ │ └── llama3-1t + │ │ ├── code # captured per-leaf │ │ ├──results.json │ │ ├── YYYYMMDD_HHmmss │ │ │ └── dlio_config │ │ ... (10x Runs for Read and Write. May be combined in a single run) │ │ └── YYYYMMDD_HHmmss │ │ └── dlio_config - │ └── vdb_bench - | ├── AiSAQ - │ | ├── YYYYMMDD_HHmmss - │ | │ └── summary.json - │ | ... (5x Runs total) - │ | └── YYYYMMDD_HHmmss - │ | └── summary.json - | ├── DiskANN - │ | ├── YYYYMMDD_HHmmss - │ | │ └── summary.json - │ | ... (5x Runs total) - │ | └── YYYYMMDD_HHmmss - │ | └── summary.json + │ └── vector_database + | ├── AISAQ + │ | ├── code # captured per-leaf + │ | ├── datagen + │ | │ └── YYYYMMDD_HHmmss + │ | │ └── summary.json + │ | └── run + │ | ├── YYYYMMDD_HHmmss + │ | │ └── summary.json + │ | ... (5x Runs total) + │ | └── YYYYMMDD_HHmmss + │ | └── summary.json + | ├── DISKANN + │ | ├── code # captured per-leaf + │ | ├── datagen + │ | │ └── YYYYMMDD_HHmmss + │ | │ └── summary.json + │ | └── run + │ | ├── YYYYMMDD_HHmmss + │ | │ └── summary.json + │ | ... (5x Runs total) + │ | └── YYYYMMDD_HHmmss + │ | └── summary.json | └── HNSW - │ ├── YYYYMMDD_HHmmss - │ │ └── summary.json - │ ... (5x Runs total) - │ └── YYYYMMDD_HHmmss - │ └── summary.json + │ ├── code # captured per-leaf + │ ├── datagen + │ │ └── YYYYMMDD_HHmmss + │ │ └── summary.json + │ └── run + │ ├── YYYYMMDD_HHmmss + │ │ └── summary.json + │ ... (5x Runs total) + │ └── YYYYMMDD_HHmmss + │ └── summary.json └── systems ├──system-name-1.yaml ├──system-name-1.pdf @@ -347,7 +398,11 @@ root_folder (or any name you prefer) ## 3.6. Training OPEN versus CLOSED Options -3.6.1. **trainingClosedSubmissionChecksum** -- For CLOSED submissions of this benchmark, the MLPerf Storage codebase cannot be changed, so the *submission validation checker* SHOULD do an `md5sum` of the code directory hierachy in the submission package and verify that that matches a precalculated checksum stored as a literal in the validator's codebase. +3.6.1. **trainingClosedSubmissionChecksum** -- For CLOSED submissions of this benchmark, the MLPerf Storage codebase must not be changed. The *submission validation checker* enforces this with a layered check: + + (a) **Self-consistency check (always runs):** the validator recomputes the captured `code/` tree's MD5 (per the exclusion set documented in §2.1.6) and compares it against the recorded "hash" in `.code-hash.json`. This detects post-capture tampering of the submission package itself. + + (b) **Upstream-identity check (CLOSED only):** the validator additionally compares the captured tree's MD5 against a pinned digest from `REFERENCE_CHECKSUMS` (or a value supplied via the `--reference-checksum` CLI flag). When no pinned digest is configured, the upstream-identity check is skipped with a single warning per run; the self-consistency check (a) still runs and can still fail. The pinned digest, when present, must be computed against the same exclusion set as the runtime capture (currently dotfiles, dotdirs, `test/`, `tests/`, `__pycache__/`, `.egg-info/`, `*.pyc`, and `.code-hash.json` itself). 3.6.2. **trainingClosedSubmissionParameters** -- For CLOSED submissions of this benchmark, only a small number of parameters can be modified, and those parameters are listed in the table below. Any other parameters being modified must generate a message and fail the validation. @@ -490,7 +545,7 @@ System: ## 5.3. VDB Run Options -5.3.1. **vdbRunCount** -- Within each *index directory* (named "DiskANN", "HNSW", or "AiSAQ") under "vdb_bench", there must be exactly five *timestamp directories*, each containing a "summary.json". (see Rules.md 2.1 directory diagram) +5.3.1. **vdbRunCount** -- Within each `vector_database//run/` directory (where `` is one of the UPPERCASE tokens `DISKANN`, `HNSW`, or `AISAQ`), there must be exactly five `` timestamp directories, each containing a `summary.json`. The count rule applies to query runs only — `datagen` is governed by §5.2. (see §2.1.27 directory diagram.) 5.3.2. **vdbRecallReported** -- Each run's `summary.json` (or its rank-local `recall_stats.json`) must report a recall value computed outside the timed query loop. The *submission validator* must verify a recall field is present and that recall meets or exceeds the minimum recall target defined for the chosen scale/metric. @@ -510,16 +565,17 @@ System: ## 5.6. VDB OPEN versus CLOSED Options -> **Index type token convention.** The index type is recorded and validated using the -> uppercase token (`DISKANN`, `HNSW`, `AISAQ`) defined by `VDB_INDEX_TYPES_CLOSED` in -> `mlpstorage_py/config.py`. The corresponding *index directory* names in the §2.1 -> directory diagram use the display spellings "DiskANN", "HNSW", and "AiSAQ". +> **Index type token convention.** The index type is recorded, validated, and +> stored on disk using the uppercase token (`DISKANN`, `HNSW`, `AISAQ`) defined +> by `VDB_INDEX_TYPES_CLOSED` in `mlpstorage_py/config.py`. The same token is +> used by the CLI (`--index-type`), in `summary.json.index_type`, and as the +> index directory name in the §2.1 directory diagram. -5.6.1. **vdbClosedSubmissionChecksum** -- For CLOSED submissions of this benchmark, the MLPerf Storage codebase cannot be changed, so the *submission validation checker* SHOULD do an `md5sum` of the code directory hierarchy in the submission package and verify that it matches a precalculated checksum stored as a literal in the validator's codebase. +5.6.1. **vdbClosedSubmissionChecksum** -- For CLOSED VDB submissions, the *submission validator* enforces the same layered code-image check defined in §3.6.1: self-consistency against `.code-hash.json` always, plus upstream-identity against `REFERENCE_CHECKSUMS` (or `--reference-checksum`) for CLOSED. See §2.1.6 for the `.code-hash.json` schema and exclusion set. 5.6.2. **vdbClosedDatabaseBackend** -- For CLOSED submissions, the vector database backend must be Milvus. The *submission validator* must read the `database.database` field from the run's `config.json`/`summary.json` and fail validation if any backend other than `milvus` is recorded. -5.6.3. **vdbClosedIndexTypes** -- For CLOSED submissions, the index type must be one of exactly three supported types: `DISKANN`, `HNSW`, or `AISAQ` (matching `VDB_INDEX_TYPES_CLOSED`). The *submission validator* must read the `index_type` field and the index directory name under "vdb_bench" and fail validation if any other index type (e.g. `IVF_FLAT`, `IVF_SQ8`, or `FLAT`) is recorded. Within these three index types, the submitter is free to choose the metric type and any index-specific build and search parameters (see 5.6.4). +5.6.3. **vdbClosedIndexTypes** -- For CLOSED submissions, the index type must be one of exactly three supported types: `DISKANN`, `HNSW`, or `AISAQ` (matching `VDB_INDEX_TYPES_CLOSED`). The *submission validator* must read the `index_type` field and the index directory name under "vector_database" and fail validation if any other index type (e.g. `IVF_FLAT`, `IVF_SQ8`, or `FLAT`) is recorded. Within these three index types, the submitter is free to choose the metric type and any index-specific build and search parameters (see 5.6.4). 5.6.4. **vdbClosedSubmissionParameters** -- For CLOSED submissions of this benchmark, the database backend is fixed to Milvus (see 5.6.2) and the index type is restricted to `DISKANN`, `HNSW`, or `AISAQ` (see 5.6.3), but the submitter may freely choose the metric type and all index-specific build/search parameters for those three index types, plus the load and run parameters listed in the table below. Any other parameter being modified, any unsupported index type, or any attempt to substitute a different database backend must generate a message and fail the validation. diff --git a/mlpstorage_py/benchmarks/base.py b/mlpstorage_py/benchmarks/base.py index 903fdeeb..458c63f5 100755 --- a/mlpstorage_py/benchmarks/base.py +++ b/mlpstorage_py/benchmarks/base.py @@ -801,7 +801,18 @@ def generate_output_location(self) -> str: """ if not self.BENCHMARK_TYPE: raise ValueError('No benchmark specified. Unable to generate output location') - return generate_output_location(self, self.run_datetime) + # Thread the validated orgname/systemname stashed by + # capture_or_verify_code_image (code_image.py: args._validated_orgname / + # args._validated_systemname) so generate_output_location's + # OPEN/CLOSED ConfigurationError path doesn't fire. For legacy / + # whatif modes these attrs are absent (getattr default None) and the + # function's mode check skips the orgname/systemname requirement. + return generate_output_location( + self, + self.run_datetime, + orgname=getattr(self.args, "_validated_orgname", None), + systemname=getattr(self.args, "_validated_systemname", None), + ) _COLLISION_BUMP_BUDGET = DEFAULT_COLLISION_BUMP_BUDGET diff --git a/mlpstorage_py/config.py b/mlpstorage_py/config.py index 702f7122..d36e9a2d 100755 --- a/mlpstorage_py/config.py +++ b/mlpstorage_py/config.py @@ -118,6 +118,7 @@ def get_datetime_string(): # VDB Benchmark Configuration VDB_INDEX_TYPES = ["DISKANN", "HNSW", "AISAQ", "IVF_FLAT", "IVF_SQ8", "FLAT"] VDB_INDEX_TYPES_CLOSED = ["DISKANN", "HNSW", "AISAQ"] + VDB_ORCHESTRATION_MODES = ["ssh", "mpi"] VDB_BENCHMARK_MODES = ["timed", "query_count", "sweep"] # Vector-database engines. Only milvus is wired up today; the slot exists so @@ -154,6 +155,8 @@ class EXIT_CODE(enum.IntEnum): SUCCESS = 0 GENERAL_ERROR = 1 INVALID_ARGUMENTS = 2 + # CAP/VALR failure exit code (per 02-CONTEXT.md D-22). Aliased with INVALID_ARGUMENTS=2 for ergonomic naming at the typed-exception → exit mapping in main.py. + CODE_IMAGE_ERROR = 2 FILE_NOT_FOUND = 3 PERMISSION_DENIED = 4 CONFIGURATION_ERROR = 5 diff --git a/mlpstorage_py/errors.py b/mlpstorage_py/errors.py index 9b4bd441..bb4999d8 100755 --- a/mlpstorage_py/errors.py +++ b/mlpstorage_py/errors.py @@ -147,6 +147,13 @@ def __init__(self, message: str, parameter: str = None, expected=expected, actual=actual ) + # Expose the missing/invalid parameter name as a direct attribute so + # the CLI dispatch layer (and tests) can inspect it without poking at + # the structured-error context dict. Documented use case: the Phase 2 + # generate_output_location trust-contract raises ConfigurationError + # with parameter="orgname" or "systemname" so the dispatch helper + # can map it back to the MLPSTORAGE_* env-var the user must set. + self.parameter = parameter @staticmethod def _default_suggestion(code: ErrorCode) -> str: diff --git a/mlpstorage_py/main.py b/mlpstorage_py/main.py index 81630814..0d741097 100755 --- a/mlpstorage_py/main.py +++ b/mlpstorage_py/main.py @@ -39,6 +39,7 @@ ) from mlpstorage_py.validation_helpers import validate_benchmark_environment from mlpstorage_py.progress import progress_context +from mlpstorage_py.submission_checker.tools.code_image import capture_or_verify_code_image, CodeImageError logger = setup_logging("MLPerfStorage") signal_received = False @@ -202,6 +203,18 @@ def run_benchmark(args, run_datetime): else: logger.warning("Skipping environment validation (--skip-validation flag)") + # Capture/verify code image BEFORE benchmark instantiation (Phase 2 D-07). + # Helper internally gates on (args.mode, args.command) per D-10, so it is + # safe to call unconditionally — non-result-generating commands no-op. + # Helper also owns ALL env-var reading and validation (POSIX regex + inline + # `.`/`..` path-traversal guard) — see Plan 02 REVIEWS.md consensus finding. + with progress_context( + "Capturing or verifying code image...", + total=None, + logger=logger + ) as (update, set_desc): + capture_or_verify_code_image(args, os.environ, logger) + program_switch_dict = dict( training=TrainingBenchmark, checkpointing=CheckpointingBenchmark, @@ -404,6 +417,15 @@ def main(): logger.info(f"Suggestion: {e.suggestion}") return EXIT_CODE.FAILURE + except CodeImageError as e: + # Phase 2 D-22: code-image capture/verify failures (incl. MissingHashFile, + # MalformedHashFile, hash-mismatch CodeImageError) map to a dedicated + # exit code distinct from generic FAILURE so CI/scripts can detect them. + # CodeImageError is NOT a MLPStorageException subclass, so it requires + # an explicit handler ordered BEFORE the MLPStorageException catch-all. + logger.error(str(e)) + return EXIT_CODE.CODE_IMAGE_ERROR + except MLPStorageException as e: # Catch-all for any other custom exceptions logger.error(str(e)) @@ -424,8 +446,12 @@ def main(): logger.error(f"Unexpected error: {str(e)}") logger.error(format_error('INTERNAL_ERROR', error=str(e))) - # Show traceback if in debug mode - if MLPS_DEBUG: + # Show traceback if in debug mode. MLPS_DEBUG is the env-var path + # (read at import time); also check `--debug` directly via sys.argv + # so the CLI flag emits a trace even though `args` is not in scope + # here. `--debug` is store_true so a bare-token check suffices. + debug_cli = '--debug' in sys.argv + if MLPS_DEBUG or debug_cli: logger.debug("Stack trace:") traceback.print_exc() else: diff --git a/mlpstorage_py/rules/utils.py b/mlpstorage_py/rules/utils.py index a1f19c69..8afb9f7f 100755 --- a/mlpstorage_py/rules/utils.py +++ b/mlpstorage_py/rules/utils.py @@ -6,10 +6,44 @@ """ import os +import re import sys from typing import Tuple, List, Optional from mlpstorage_py.config import BENCHMARK_TYPES, DATETIME_STR +from mlpstorage_py.errors import ConfigurationError, ErrorCode + +# Env-var names used by the Phase 2 CLI dispatch layer to source orgname/systemname (D-01, D-02). +# generate_output_location itself does NOT read these; the helper in +# mlpstorage_py/submission_checker/tools/code_image.py reads + validates them and threads +# the values through as keyword arguments. The names are exported here so the helper has a +# single source of truth for the env-var spelling. +MLPSTORAGE_ORGNAME_ENVVAR = "MLPSTORAGE_ORGNAME" +MLPSTORAGE_SYSTEMNAME_ENVVAR = "MLPSTORAGE_SYSTEMNAME" + +# Each path segment appended to results_dir by generate_output_location must +# match this — POSIX-safe alphanumeric plus '.', '_', '-' — and must not be +# '.' or '..'. Blocks path-traversal ('../') and absolute-path resets ('/') +# at the trust boundary between args/env-var input and os.path.join, even +# for callers that bypass the CLI's argparse choices= validation. +_SAFE_PATH_COMPONENT_RE = re.compile(r"^[A-Za-z0-9._-]+$") + + +def _check_safe_path_component(name: str, value: str) -> None: + """Raise ValueError if value is not safe as a single path segment. + + Caller handles None/empty upstream as a separate "missing required arg" + failure mode; this helper assumes value is a non-empty string. + """ + if value in (".", ".."): + raise ValueError( + f"{name}={value!r} is not a safe path component (reserved name)" + ) + if not _SAFE_PATH_COMPONENT_RE.match(value): + raise ValueError( + f"{name}={value!r} is not a safe path component " + f"(must match {_SAFE_PATH_COMPONENT_RE.pattern})" + ) def calculate_training_data_size(args, cluster_information, dataset_params, reader_params, logger, @@ -118,28 +152,61 @@ def calculate_training_data_size(args, cluster_information, dataset_params, read return int(required_file_count), int(required_subfolders_count), int(total_disk_bytes) -def generate_output_location(benchmark, datetime_str=None, **kwargs) -> str: +def generate_output_location( + benchmark, + datetime_str=None, + *, + orgname: Optional[str] = None, + systemname: Optional[str] = None, + **kwargs, +) -> str: """ Generate a standardized output location for benchmark results. Output structure follows this pattern: - RESULTS_DIR: - : - : - : - : - run_ (Optional) + + CLOSED (args.mode == "closed"): + /closed////// + + OPEN (args.mode == "open"): + /open//results////// + + Legacy (args.mode not in {"closed", "open"}, or attribute missing — + e.g. whatif, programmatic callers from tests): + ///// + + The per-``BENCHMARK_TYPES`` tail (training/checkpointing/vector_database/ + kv_cache) is unchanged below the new prefix. Args: benchmark: Benchmark instance. datetime_str: Optional datetime string for the run. - **kwargs: Additional benchmark-specific parameters. + orgname: Keyword-only. Submitter organization name; required when + ``benchmark.args.mode`` is "closed" or "open". The CLI dispatch + layer (Plan 02-02) reads ``MLPSTORAGE_ORGNAME`` from the + environment, validates it per Rules.md §2.1.1, and threads the + validated value through as this keyword argument. This function + does NOT read ``os.environ`` — passing the value explicitly is a + trust-contract requirement so programmatic callers (tests, + future tooling) receive a typed ``ConfigurationError`` if they + forget to thread it through, rather than a hidden ``KeyError``. + systemname: Keyword-only. System name; required when + ``benchmark.args.mode`` is "open". Same trust-contract semantics + as ``orgname``; sourced from ``MLPSTORAGE_SYSTEMNAME`` by the + dispatch layer. + **kwargs: Additional benchmark-specific parameters (reserved). Returns: Full path to the output location. Raises: - ValueError: If required parameters are missing. + ValueError: If required parameters are missing (e.g. ``args.model`` + for training/checkpointing benchmarks). + ConfigurationError: If ``benchmark.args.mode`` is "closed" or "open" + but ``orgname`` (and, for "open", ``systemname``) was not threaded + through by the caller. The ``parameter`` attribute identifies the + missing kwarg; the ``suggestion`` field references the + ``MLPSTORAGE_*`` env-var the dispatch layer must read. """ if datetime_str is None: datetime_str = DATETIME_STR @@ -151,17 +218,68 @@ def generate_output_location(benchmark, datetime_str=None, **kwargs) -> str: else: run_number = 0 + # New D-03 prefix: insert {closed|open}/[/results/]/ + # before the legacy per-type chain. The values are explicit kwargs threaded + # by the CLI dispatch layer (Plan 02-02); env-var reading is owned by that + # helper, not this function (see module-level constants above for the + # env-var-name source of truth). + mode = getattr(benchmark.args, "mode", None) + if mode in ("closed", "open"): + if not orgname: + raise ConfigurationError( + "orgname is required when args.mode in {closed, open} but was " + "not provided to generate_output_location", + parameter="orgname", + suggestion=( + f"The CLI dispatch layer should read the " + f"{MLPSTORAGE_ORGNAME_ENVVAR} environment variable and " + "thread the validated value through as the orgname " + "keyword. Programmatic callers must pass orgname= " + "explicitly." + ), + code=ErrorCode.CONFIG_MISSING_REQUIRED, + ) + _check_safe_path_component("orgname", orgname) + output_location = os.path.join(output_location, mode, orgname) + + if mode == "open": + if not systemname: + raise ConfigurationError( + "systemname is required when args.mode == 'open' but was " + "not provided to generate_output_location", + parameter="systemname", + suggestion=( + f"The CLI dispatch layer should read the " + f"{MLPSTORAGE_SYSTEMNAME_ENVVAR} environment " + "variable and thread the validated value through " + "as the systemname keyword. Programmatic callers " + "must pass systemname= explicitly." + ), + code=ErrorCode.CONFIG_MISSING_REQUIRED, + ) + _check_safe_path_component("systemname", systemname) + output_location = os.path.join(output_location, "results", systemname) + + # datetime_str is built into every per-type path below; validate once here. + _check_safe_path_component("datetime_str", datetime_str) + # Handle different benchmark types if benchmark.BENCHMARK_TYPE == BENCHMARK_TYPES.training: if not hasattr(benchmark.args, "model"): raise ValueError("Model name is required for training benchmark output location") + _check_safe_path_component("model", benchmark.args.model) + _check_safe_path_component("command", benchmark.args.command) output_location = os.path.join(output_location, benchmark.BENCHMARK_TYPE.name) output_location = os.path.join(output_location, benchmark.args.model) output_location = os.path.join(output_location, benchmark.args.command) output_location = os.path.join(output_location, datetime_str) elif benchmark.BENCHMARK_TYPE == BENCHMARK_TYPES.vector_database: + # Results split by index_type because AISAQ is not comparable to + # DISKANN/HNSW — they must live in separate on-disk trees so + # submission validation and downstream tooling never collate them + # (per Rules.md §2.1.27). engine = getattr(benchmark.args, "vdb_engine", None) if not engine: raise ValueError( @@ -177,7 +295,10 @@ def generate_output_location(benchmark, datetime_str=None, **kwargs) -> str: "VectorDB index is required for output location " "(set --vdb-index on the CLI)." ) - + + _check_safe_path_component("vdb_engine", engine) + _check_safe_path_component("vdb_index", vdb_index) + _check_safe_path_component("command", benchmark.args.command) output_location = os.path.join(output_location, benchmark.BENCHMARK_TYPE.name) output_location = os.path.join(output_location, engine) output_location = os.path.join(output_location, vdb_index) @@ -192,6 +313,8 @@ def generate_output_location(benchmark, datetime_str=None, **kwargs) -> str: "args.model before calling generate_output_location " "(KVCacheBenchmark.__init__ defaults this from KVCACHE_MODEL_DEFAULT)." ) + _check_safe_path_component("model", model) + _check_safe_path_component("command", benchmark.args.command) output_location = os.path.join(output_location, benchmark.BENCHMARK_TYPE.name) output_location = os.path.join(output_location, model) output_location = os.path.join(output_location, benchmark.args.command) @@ -201,6 +324,7 @@ def generate_output_location(benchmark, datetime_str=None, **kwargs) -> str: if not hasattr(benchmark.args, "model"): raise ValueError("Model name is required for checkpointing benchmark output location") + _check_safe_path_component("model", benchmark.args.model) output_location = os.path.join(output_location, benchmark.BENCHMARK_TYPE.name) output_location = os.path.join(output_location, benchmark.args.model) output_location = os.path.join(output_location, datetime_str) diff --git a/mlpstorage_py/submission_checker/checks/helpers.py b/mlpstorage_py/submission_checker/checks/helpers.py index 767d2374..af5d3481 100644 --- a/mlpstorage_py/submission_checker/checks/helpers.py +++ b/mlpstorage_py/submission_checker/checks/helpers.py @@ -1,25 +1,43 @@ """Shared pure-function helpers for Phase 2 check methods. This module is LOG-FREE: helpers return status tuples and never call -``log_violation`` or ``self.log.error`` directly. Callers emit violations -using the standard ``BaseCheck.log_violation`` / ``warn_violation`` pattern +``log_violation`` or ``self.log.error`` directly (with the exception of +``_check_code_image_layered``, which invokes a caller-supplied +``log_violation_cb`` so the caller's rule ID/name are carried into the +violation message — see CD-04 below). Callers emit violations using the +standard ``BaseCheck.log_violation`` / ``warn_violation`` pattern (Pitfall #11, PROJECT.md accumulate-don't-abort principle). Exports: DF_HEADER_RE — compiled regex matching the ``df`` header line (D-B1) _check_filesystem_separation — filesystem-separation helper (D-B1..B5) + _check_code_image_layered — benchmark-agnostic layered code-image helper + (Phase 4 CD-04; shared by §3.6.1 and §5.6.1) _pair_checkpoint_runs — write/read run pairing helper (D-D2) _parse_iso_gap — ISO-timestamp gap helper (D-D2, CHKPT-03) References: - D-B1..B7 in Phase 2 CONTEXT.md (df parsing, longest-prefix mount match) - D-D2 in Phase 2 CONTEXT.md (pairing write/read checkpoint runs) + - Phase 4 CONTEXT.md D-06 / CD-04 (layered helper extraction) - RESEARCH.md §Shared Helpers """ import datetime +import logging import os import re +from pathlib import Path + +_LOG = logging.getLogger(__name__) + +from ..tools.code_checksum import compute_code_tree_md5 +from ..tools.code_image import ( + verify_image_self_consistent, + CodeImageError, + MissingHashFile, + MalformedHashFile, +) # --------------------------------------------------------------------------- @@ -103,6 +121,15 @@ def _check_filesystem_separation( # The regex match ends at the last char of "Mounted on" (before the newline), # so content[match.end():] starts with '\n'. We skip that initial newline by # starting after the end of the matched line. + # + # TODO(TODO-001): the current "scan df output of the log file" approach is + # planned to be superseded by capturing `stat -f -c '%i' "$data_dir"` per + # node at runtime — a single scalar FS identity stored alongside per-node + # metadata, compared for equality across nodes. That removes both this + # multi-line-device-name parse limitation and the substring-matching + # fragility called out in WR-06's silent-pass case. Until that migration + # lands, real submissions with wrapped device names hard-fail with + # "df output not found" (D-B4), which is the desired gap-surfacing behaviour. mounts = [] header_end = content.find("\n", match.end()) # find the end of the header line if header_end == -1: @@ -142,14 +169,141 @@ def _best_mount(realpath: str) -> str | None: data_mount = _best_mount(real_data) results_mount = _best_mount(real_results) - # If either path cannot be matched to a mount → cannot determine violation; pass + # If either path cannot be matched to a mount → indeterminate. + # Emit a warning so the gap is grep-visible (a typo'd data_dir would + # otherwise silent-pass this check). The pass return is preserved so we + # don't false-positive on weird mount tables that nonetheless contain + # a legitimate data_dir / results_dir pair the regex can't resolve. if data_mount is None or results_mount is None: + _LOG.warning( + "_check_filesystem_separation: could not match data_dir=%s " + "(realpath %s) or results_dir=%s (realpath %s) to any df mount " + "in %s; treating as pass (data_mount=%s, results_mount=%s)", + data_dir, real_data, results_dir, real_results, logfile_path, + data_mount, results_mount, + ) return (True, True) # Same mount → violation return (data_mount != results_mount, True) +# --------------------------------------------------------------------------- +# _check_code_image_layered (Phase 4 CD-04) +# --------------------------------------------------------------------------- + +def _check_code_image_layered( + code_path: str, + division: str, + expected: str | None, + log, + log_violation_cb, + rule_id: str, + rule_name: str, +) -> bool: + """Benchmark-agnostic layered code-image check (self-consistency + upstream-identity). + + Mirrors the two inner branches of STRUCT-06 + (``submission_structure_checks.code_directory_contents_check``) so the + same layered model is enforced under multiple rule IDs without duplicating + the implementation across check classes: + + * ``2.1.6 codeDirectoryContents`` — STRUCT-06 itself, calls + ``self.log_violation`` directly with its own ID/name. + * ``3.6.1 trainingClosedSubmissionChecksum`` — TrainingCheck, calls this + helper with the 3.6.1 rule ID/name pair. + * ``5.6.1 vdbClosedSubmissionChecksum`` — VdbCheck, calls this helper + with the 5.6.1 rule ID/name pair. + + The duplication of rule IDs is intentional (Phase 4 D-06): downstream + tooling must be able to tell whether a code-image mismatch fired under + §2.1.6 (structural), §3.6.1 (Training CLOSED), or §5.6.1 (VDB CLOSED). The + *implementation* of the check is unified here (CD-04); the *attribution* + stays per-rule via the caller-supplied ``rule_id`` / ``rule_name``. + + The helper performs the same two-step check defined for STRUCT-06 at + ``submission_structure_checks.py:442-470``: + + 1. Self-consistency: try ``verify_image_self_consistent(code_path, log)``. + If it returns False, log a violation and set ``valid = False``. Catch + ``MissingHashFile`` / ``MalformedHashFile`` / ``CodeImageError`` and log + the exception message as a violation. + 2. Upstream-identity (CLOSED only, D-06 + D-07): if ``division == "closed"`` + AND ``expected is not None``, compute ``compute_code_tree_md5`` and + compare against ``expected``. Mismatch → log a violation. + + Args: + code_path: Absolute on-disk path to the ``code/`` directory to validate. + division: ``"closed"`` or ``"open"``. The upstream-identity branch fires + only for ``"closed"`` (matches STRUCT-06 L467 + D-06). + expected: The reference digest returned by + ``Config.get_reference_checksum()``. ``None`` means upstream-identity + is skipped (matches STRUCT-06 L417 + D-12 single-warning behavior). + log: Logger instance, passed through to ``verify_image_self_consistent`` + and ``compute_code_tree_md5``. + log_violation_cb: A callable with the same signature as + ``BaseCheck.log_violation`` — + ``(rule_id, rule_name, path, fmt, *args)``. Decoupling the helper + from a specific check class is what makes it benchmark-agnostic. + rule_id: The caller's Rules.md rule ID (e.g., ``"3.6.1"``, ``"5.6.1"``). + Passed through to every ``log_violation_cb`` call so violations + carry the CALLER's rule ID, not a generic helper ID. + rule_name: The caller's camelCase Rules.md rule name (e.g., + ``"trainingClosedSubmissionChecksum"``, ``"vdbClosedSubmissionChecksum"``). + + Returns: + ``True`` if every branch passed; ``False`` if any violation was logged. + """ + valid = True + # When .code-hash.json is absent, the per-tree integrity anchor does not + # exist — the upstream-identity branch would re-walk the entire tree and + # log a SECOND, contradictory violation per leaf with no diagnostic value + # over the first ("missing .code-hash.json"). MalformedHashFile and + # CodeImageError are different: the JSON parses but the hash mismatches + # or refers to an absent root — keep dual-violation behavior for those + # so the upstream-identity walk still adds signal. + hashfile_present = True + + # 1. Self-consistency branch (STRUCT-06 L448-L464 analog). + try: + if not verify_image_self_consistent(Path(code_path), log): + log_violation_cb( + rule_id, rule_name, code_path, + "code tree hash does not match .code-hash.json at %s", + code_path, + ) + valid = False + except MissingHashFile as e: + hashfile_present = False + log_violation_cb( + rule_id, rule_name, code_path, + "%s", str(e), + ) + valid = False + except (MalformedHashFile, CodeImageError) as e: + log_violation_cb( + rule_id, rule_name, code_path, + "%s", str(e), + ) + valid = False + + # 2. Upstream-identity branch (STRUCT-06 L466-L476 analog; CLOSED + expected only). + # Skip the O(tree) re-walk when no .code-hash.json anchored step 1 — the + # caller already knows the leaf is broken; a redundant violation here + # just adds noise without surfacing new information. + if division == "closed" and expected is not None and hashfile_present: + digest = compute_code_tree_md5(code_path, log) + if digest != expected: + log_violation_cb( + rule_id, rule_name, code_path, + "code tree MD5 mismatch: expected %s, got %s", + expected, digest, + ) + valid = False + + return valid + + # --------------------------------------------------------------------------- # _pair_checkpoint_runs # --------------------------------------------------------------------------- diff --git a/mlpstorage_py/submission_checker/checks/submission_structure_checks.py b/mlpstorage_py/submission_checker/checks/submission_structure_checks.py index a7d44d41..d330c4fd 100644 --- a/mlpstorage_py/submission_checker/checks/submission_structure_checks.py +++ b/mlpstorage_py/submission_checker/checks/submission_structure_checks.py @@ -21,11 +21,18 @@ import json import os import re +from pathlib import Path from .base import BaseCheck from ..configuration.configuration import Config from ..rule_registry import rule from ..tools.code_checksum import compute_code_tree_md5 +from ..tools.code_image import ( + verify_image_self_consistent, + CodeImageError, + MissingHashFile, + MalformedHashFile, +) from ..utils import list_dir, list_files from ..parsers.yaml_parser import YamlParser @@ -38,8 +45,34 @@ # Allowed top-level divisions (case-sensitive, PITFALLS.md #2) _VALID_DIVISIONS = frozenset({"closed", "open"}) -# Required submitter subdirectories (case-sensitive set equality) -_REQUIRED_SUBMITTER_SUBDIRS = frozenset({"code", "results", "systems"}) +# Benchmark-type directory names whose OPEN leaf shape has no per-leaf +# segment between / and code/ — code/ lives directly at /code/. +# +# vector_database is NOT in this set: AISAQ results are not comparable to +# DISKANN/HNSW, so its leaf shape is vector_database//code/ — +# same 3-level walk as training () and checkpointing (). The +# index directory uses the UPPERCASE token (DISKANN/HNSW/AISAQ), matching +# args.index_type and summary.json.index_type. +# +# kv_cache stays here transitionally — its directory/file structure below +# the / prefix will be finalized in a follow-up plan. Once the +# per-(model, operation) split is specified, this entry will move into the +# standard 3-level walk too. +# +# Mirror set kept inline (rather than imported from tools.code_image) to +# avoid pulling the helper module's runtime dependencies into the validator. +_OPEN_TYPES_WITHOUT_MODEL = frozenset({"kv_cache"}) + +# Mode-aware required submitter-level subdirectory sets per Rules.md §2.1.5 split (D-17). +# CLOSED: {code, results, systems} at the submitter level. +# OPEN: {results, systems} at the submitter level; code/ lives at each +# results//// leaf (see code_directory_contents_check +# and Rules.md §2.1.5.b / §2.1.27 OPEN subtree). +_REQUIRED_SUBMITTER_SUBDIRS_CLOSED = frozenset({"code", "results", "systems"}) +_REQUIRED_SUBMITTER_SUBDIRS_OPEN = frozenset({"results", "systems"}) + +# Legacy alias for CLOSED — see _REQUIRED_SUBMITTER_SUBDIRS_CLOSED. +_REQUIRED_SUBMITTER_SUBDIRS = _REQUIRED_SUBMITTER_SUBDIRS_CLOSED # Valid workload categories under results// _VALID_WORKLOAD_CATEGORIES = frozenset({"training", "checkpointing"}) @@ -110,6 +143,57 @@ def _iter_submitter_dirs(self): for submitter in list_dir(div_path): yield division, submitter, os.path.join(div_path, submitter) + def _iter_open_code_dirs(self, submitter_path): + """Yield each per-leaf code/ path under an OPEN submitter (D-15). + + The leaf shape depends on the benchmark type: + + - training, checkpointing → results////code/ + (runtime output is keyed per model). + - vector_database → results//vector_database//code/ + (results split by index type — AISAQ/DISKANN/HNSW results are not + comparable and live in separate trees; the index directory uses + the UPPERCASE token, matching args.index_type). + - kv_cache → results///code/ + (transitional shape — kv_cache directory structure below the + / prefix will be finalized in a follow-up plan). + + Per Rules.md §2.1.27 OPEN subtree, code/ lives at each leaf rather + than at the submitter level. This generator yields the absolute + code/ path for every leaf — whether or not the directory currently + exists on disk (the caller decides what to do with the path). + """ + results = os.path.join(submitter_path, "results") + if not os.path.isdir(results): + return + for sys_name in list_dir(results): + # Skip dot-prefixed entries (.git/, .github/, .cache/, .DS_Store, etc.) — + # every sibling check in this file already filters them, and yielding a + # synthetic code/ path under one produces a spurious 2.1.6 violation. + if sys_name.startswith("."): + continue + sys_path = os.path.join(results, sys_name) + if not os.path.isdir(sys_path): + continue + for wtype in list_dir(sys_path): + if wtype.startswith("."): + continue + wtype_path = os.path.join(sys_path, wtype) + if not os.path.isdir(wtype_path): + continue + if wtype in _OPEN_TYPES_WITHOUT_MODEL: + # kv_cache (transitional): code/ is a direct child of + # / — no per-leaf segment yet (see _OPEN_TYPES_WITHOUT_MODEL). + yield os.path.join(wtype_path, "code") + continue + for model in list_dir(wtype_path): + if model.startswith("."): + continue + model_path = os.path.join(wtype_path, model) + if not os.path.isdir(model_path): + continue + yield os.path.join(model_path, "code") + def _load_json_safe(self, json_path): """Return parsed JSON dict or None on any error (silently).""" try: @@ -255,26 +339,41 @@ def closed_submitter_directory_check(self): @rule("2.1.5", "requiredSubdirectories") def required_subdirectories_check(self): - """STRUCT-05: submitter dir must contain EXACTLY {code, results, systems}. + """STRUCT-05: submitter dir must contain EXACTLY the required set for its division. + + Per Rules.md §2.1.5 split (D-17): + - CLOSED submitter dir: {code, results, systems} + - OPEN submitter dir: {results, systems} (code/ lives per-leaf in OPEN) Dot-prefixed entries are silently skipped (e.g. .DS_Store, .cache/). - When an unexpected subdirectory itself contains some of {code, results, - systems}, the diagnostic includes a wrapping hint — this catches the - common v2.0 submitter mistake of nesting the package one level deeper - than the spec requires (e.g. closed//benchmarks/{code, - results, systems}/ instead of closed//{code, results, - systems}/). + When an unexpected subdirectory itself contains some of the division's + required set, the diagnostic includes a wrapping hint — this catches + the common v2.0 submitter mistake of nesting the package one level + deeper than the spec requires. + + Violation messages route through the sub-rule anchors + `requiredSubdirectoriesClosed` (CLOSED) and `requiredSubdirectoriesOpen` + (OPEN), matching the §2.1.5.a / §2.1.5.b sub-rules in Rules.md. The + rule-id passed to `log_violation` stays `"2.1.5"` (the top-level rule + number is unchanged — only the per-violation sub-rule anchor splits). """ valid = True for division, submitter, sub_path in self._iter_submitter_dirs(): + if division == "closed": + required = _REQUIRED_SUBMITTER_SUBDIRS_CLOSED + anchor = "requiredSubdirectoriesClosed" + else: # open + required = _REQUIRED_SUBMITTER_SUBDIRS_OPEN + anchor = "requiredSubdirectoriesOpen" + actual = {e for e in list_dir(sub_path) if not e.startswith(".")} - missing = _REQUIRED_SUBMITTER_SUBDIRS - actual - extra = actual - _REQUIRED_SUBMITTER_SUBDIRS + missing = required - actual + extra = actual - required for m in sorted(missing): self.log_violation( - "2.1.5", "requiredSubdirectories", + "2.1.5", anchor, os.path.join(sub_path, m), "required subdirectory %r missing from %s/%s", m, division, submitter, @@ -288,7 +387,7 @@ def required_subdirectories_check(self): nested = { n for n in list_dir(extra_path) if not n.startswith(".") } - wrapped = sorted(nested & _REQUIRED_SUBMITTER_SUBDIRS) + wrapped = sorted(nested & required) if wrapped: hint = ( "; the submission appears to be nested one level " @@ -297,11 +396,11 @@ def required_subdirectories_check(self): % (wrapped, division, submitter) ) self.log_violation( - "2.1.5", "requiredSubdirectories", + "2.1.5", anchor, extra_path, "unexpected subdirectory %r in %s/%s " - "(only code/results/systems allowed)%s", - e, division, submitter, hint, + "(allowed: %s)%s", + e, division, submitter, sorted(required), hint, ) valid = False @@ -313,45 +412,109 @@ def required_subdirectories_check(self): @rule("2.1.6", "codeDirectoryContents") def code_directory_contents_check(self): - """STRUCT-06: for CLOSED submissions, verify code/ tree MD5. - - Per D-12: when reference checksum is None, emit WARNING and return - True (does not fail the run). The no-checksum warning is hoisted out - of the per-submitter loop so an unconfigured invocation emits one - warning per run rather than one per submitter (which would spam the - report against N-submitter merged trees). + """STRUCT-06: per-tree self-consistency for CLOSED + OPEN; layered REFERENCE_CHECKSUMS for CLOSED only. + + D-11 layered model: + - CLOSED leaves: self-consistency (VALS-02) AND REFERENCE_CHECKSUMS + upstream-identity (when configured). + - OPEN leaves: self-consistency (VALS-04) only — OPEN allows source + modifications by spec, so there is no upstream digest to enforce. + + D-14: separate violations for missing-code/ (VALS-01/03) vs hash-mismatch + (VALS-02/04). D-15: walk strategy uses _iter_submitter_dirs for the + closed/ subtree (one code/ per submitter) and the nested + _iter_open_code_dirs for the open/ subtree (one code/ per + results//// leaf). + + D-12 single-warning behavior is preserved: when get_reference_checksum() + returns None AND a closed/ subtree is present, exactly one warning + fires per run, with an addendum noting that the self-consistency + check still ran on every leaf. """ valid = True - closed_path = os.path.join(self.root_path, "closed") - if not os.path.isdir(closed_path): - return valid # no closed/ — nothing to check - - expected = self.config.get_reference_checksum() - if expected is None: - self.warn_violation( - "2.1.6", "codeDirectoryContents", - closed_path, - "reference checksum not configured " - "(use --reference-checksum or populate REFERENCE_CHECKSUMS); " - "the code/ subtree cannot be validated without one", - ) - return valid # not a failure (D-12 preserved); skip per-submitter walk + expected = self.config.get_reference_checksum() # CLOSED layered check, D-11 - for submitter in list_dir(closed_path): - code_path = os.path.join(closed_path, submitter, "code") - if not os.path.isdir(code_path): - continue # STRUCT-05 will catch missing code/ + for division, submitter, sub_path in self._iter_submitter_dirs(): + if division == "closed": + code_paths = [os.path.join(sub_path, "code")] + else: # open — nested glob per D-15 + code_paths = list(self._iter_open_code_dirs(sub_path)) + + for code_path in code_paths: + if not os.path.isdir(code_path): + # VALS-01 / VALS-03 — missing code/ + self.log_violation( + "2.1.6", "codeDirectoryContents", + code_path, + "required code/ directory missing at %s", code_path, + ) + valid = False + continue - digest = compute_code_tree_md5(code_path, self.log) - if digest != expected: - self.log_violation( + # VALS-02 / VALS-04 — self-consistency (CLOSED and OPEN). + # Same accumulate-don't-abort + short-circuit-on-missing-anchor + # pattern as _check_code_image_layered (helpers.py): a missing + # .code-hash.json already invalidates the leaf; the upstream- + # identity walk below would just add a contradictory second + # violation per leaf with no diagnostic value. + hashfile_present = True + try: + if not verify_image_self_consistent(Path(code_path), self.log): + self.log_violation( + "2.1.6", "codeDirectoryContents", + code_path, + "code tree hash does not match .code-hash.json at %s", + code_path, + ) + valid = False + except MissingHashFile as e: + hashfile_present = False + self.log_violation( + "2.1.6", "codeDirectoryContents", + code_path, + "%s", str(e), + ) + valid = False + except (MalformedHashFile, CodeImageError) as e: + self.log_violation( + "2.1.6", "codeDirectoryContents", + code_path, + "%s", str(e), + ) + valid = False + + # D-11 layered: REFERENCE_CHECKSUMS upstream-identity (CLOSED only) + if division == "closed" and expected is not None and hashfile_present: + digest = compute_code_tree_md5(code_path, self.log) + if digest != expected: + self.log_violation( + "2.1.6", "codeDirectoryContents", + code_path, + "code tree MD5 mismatch: expected %s, got %s", + expected, digest, + ) + valid = False + + # D-11/D-12 preserved: emit the "not pinned" warning exactly once per + # run when REFERENCE_CHECKSUMS is unset AND a closed/ subtree exists + # AND that subtree actually contains a submitter (any non-dot entry). + # An empty closed/ — or one with only dotfiles like .DS_Store — has no + # code/ checks to skip, so the warning would be noise. + closed_path = os.path.join(self.root_path, "closed") + if expected is None and os.path.isdir(closed_path): + has_closed_submitter = any( + not name.startswith(".") + and os.path.isdir(os.path.join(closed_path, name)) + for name in list_dir(closed_path) + ) + if has_closed_submitter: + self.warn_violation( "2.1.6", "codeDirectoryContents", - code_path, - "code tree MD5 mismatch: expected %s, got %s", - expected, digest, + closed_path, + "reference checksum not configured " + "(use --reference-checksum or populate REFERENCE_CHECKSUMS); " + "upstream-identity check skipped (self-consistency check still ran)", ) - valid = False - return valid # ----------------------------------------------------------------------- @@ -471,16 +634,14 @@ def results_directory_systems_check(self): ) valid = False else: - # Parse YAML and check submission_name == name (D-17) + # Parse YAML and check submission_name == name (D-17). + # Use `or {}` rather than `.get(key, {})` so a YAML node + # that serializes as null (not just absent) also collapses + # to a safe default instead of raising AttributeError mid-chain. system_yaml = YamlParser(yaml_path, "System").get_dict() - try: - submission_name = ( - system_yaml.get("system_under_test", {}) - .get("solution", {}) - .get("submission_name") - ) - except AttributeError: - submission_name = None + sut = system_yaml.get("system_under_test") or {} + solution = sut.get("solution") or {} + submission_name = solution.get("submission_name") if submission_name != name: self.log_violation( diff --git a/mlpstorage_py/submission_checker/checks/training_checks.py b/mlpstorage_py/submission_checker/checks/training_checks.py index 09bc6cc5..55cb36ad 100644 --- a/mlpstorage_py/submission_checker/checks/training_checks.py +++ b/mlpstorage_py/submission_checker/checks/training_checks.py @@ -4,7 +4,7 @@ from ..configuration.configuration import Config from ..loader import SubmissionLogs from ..rule_registry import rule -from .helpers import _check_filesystem_separation +from .helpers import _check_filesystem_separation, _check_code_image_layered import os import hashlib @@ -66,12 +66,13 @@ def _get_benchmark_api(self) -> str: system_file = getattr(self.submissions_logs, "system_file", None) if not system_file: return "file" - return ( - system_file.get("system_under_test", {}) - .get("solution", {}) - .get("architecture", {}) - .get("benchmark_API", "file") - ) + # `.get(key, {})` only catches missing keys — if the YAML serializes + # an intermediate node as `null`, the chained .get raises AttributeError + # on NoneType. `or {}` collapses both absent and null to a safe default. + sut = system_file.get("system_under_test") or {} + solution = sut.get("solution") or {} + architecture = solution.get("architecture") or {} + return architecture.get("benchmark_API", "file") @rule("3.1.1", "trainingVerifyDatasizeUsage") def verify_datasize_usage(self): @@ -483,16 +484,52 @@ def node_capability_consistency_check(self): @rule("3.6.1", "trainingClosedSubmissionChecksum") def closed_submission_checksum(self): - """ - For CLOSED submissions, verify code directory MD5 checksum. + """For CLOSED submissions, verify code directory MD5 checksum. + (Rules.md 3.6.1) - Stub: body currently returns True (decorator-only retrofit per Plan 03-02 - Task 1 step 1 — "decorator only, no body change"). The real implementation - will leverage the QUAL-05 MD5 predicate landed in Phase 1. + Phase 4 CD-04: delegates to the shared + ``helpers._check_code_image_layered`` helper so the §3.6.1 and §5.6.1 + rules enforce an identical layered model (self-consistency + + upstream-identity) without duplicating the implementation across + check classes. STRUCT-06 (§2.1.6) keeps its own inline implementation + because it has additional surrounding logic (per-leaf walker, the + ``expected is None`` warning) that does not belong in the helper. + + Walk-up: ``self.path`` is the per-leaf training path + (``/closed//results//training/``). The + CLOSED ``code/`` lives at ``/closed//code/``, four + levels above ``self.path`` (model → type → system → results → + ````). Missing ``code/`` is NOT logged here — STRUCT-06 + already owns the VALS-01 missing-code/ violation under §2.1.6, so + re-firing under §3.6.1 would double-count. """ - # TODO - return True + if self.mode != "training": + return True + + # OPEN handled at STRUCT-06 self-consistency loop, not here. + if self.submissions_logs.loader_metadata.division != "closed": + return True + + # Walk up from /closed//results//training/ + # to /closed/, then append "code". + submitter_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(self.path)))) + code_path = os.path.join(submitter_path, "code") + + # STRUCT-06 owns missing-code/ under §2.1.6; do not duplicate the violation here. + if not os.path.isdir(code_path): + return True + + expected = self.config.get_reference_checksum() + return _check_code_image_layered( + code_path, + "closed", + expected, + self.log, + self.log_violation, + "3.6.1", + "trainingClosedSubmissionChecksum", + ) @rule("3.6.2", "trainingClosedSubmissionParameters") def closed_submission_parameters(self): diff --git a/mlpstorage_py/submission_checker/checks/vdb_checks.py b/mlpstorage_py/submission_checker/checks/vdb_checks.py index f3b1fa95..1b4e378b 100644 --- a/mlpstorage_py/submission_checker/checks/vdb_checks.py +++ b/mlpstorage_py/submission_checker/checks/vdb_checks.py @@ -1,38 +1,99 @@ -"""VdbCheck — Rules.md §5 (Vector Database) extension stub. - -Rules.md §5 is empty at Phase 3 land time. STUB-01 establishes the -extension point so ``main.py`` can instantiate the class identically to -``DirectoryCheck`` / ``TrainingCheck`` / ``CheckpointingCheck`` (D-S4), -and so a future phase can fill in real checks without touching the -``main.py`` wiring shape. - -Design constraints (Phase 3 CONTEXT.md): - -* **D-S2:** registers exactly one placeholder method - (``_section_unimplemented``) which is a no-op returning ``True``. No - ``@rule`` decorator is applied — stubs MUST contribute zero rule-ID - bindings via ``discover_rules`` (success criterion #2: stubs emit zero - violations). -* **D-S3:** this module does NOT import ``coverage_mapping``. Stubs stay - decoupled from the coverage tool; coverage advertisement lives only in - ``STUB_COVERAGE`` inside ``coverage_mapping.py``. +"""VdbCheck — Rules.md §5 (Vector Database) implementation. + +Implements all 16 rules from Rules.md §5 (5.1.1–5.6.5) as +``@rule``-decorated methods on a single ``BaseCheck`` subclass. Every +rule body guards on ``self.mode != "vector_database"`` so the check is a +no-op on non-vdb subtrees — the on-disk type-segment is ``vector_database`` +(Phase 4 D-02), so the loader at ``loader.py:99-103`` yields +``loader_metadata.mode == "vector_database"`` on those leaves. + +§5.6.1 (``vdbClosedSubmissionChecksum``) delegates to the shared +``helpers._check_code_image_layered`` (Phase 4 CD-04 + D-06) — the same +helper TrainingCheck.3.6.1 uses — so the layered self-consistency + +upstream-identity model is enforced once and attributed under the +caller's rule ID. + +Index-type rules (5.3.1, 5.6.3) compare the on-disk directory name +(UPPERCASE — e.g. ``"DISKANN"``) directly against the +``summary.json.index_type`` token (also UPPERCASE). + +Loader caveat: at Phase 4 land time, ``loader.py`` has only two branches +(``training`` and an ``else`` for checkpointing) and therefore does NOT +populate ``submissions_logs.run_files`` / ``datagen_files`` for +``vector_database`` mode. Rule bodies that depend on those fields detect the +absence and emit ``warn_violation`` so the gap is grep-visible — see +the Phase-4 invariant: "must NEVER be a ``return True`` stub." When the +loader gains a vdb branch, the warn paths drop out automatically and the +real checks fire. """ +import os + from .base import BaseCheck from ..configuration.configuration import Config from ..loader import SubmissionLogs +from ..rule_registry import rule +from .helpers import _check_code_image_layered, _check_filesystem_separation +from mlpstorage_py.config import VDB_INDEX_TYPES_CLOSED -class VdbCheck(BaseCheck): - """Stub check class for Rules.md §5 (Vector Database) rules. +# Required latency / throughput fields each run's summary.json must report (§5.3.4). +_REQUIRED_METRIC_FIELDS = ( + "throughput_qps", + "mean_latency_ms", + "p95_latency_ms", + "p99_latency_ms", + "p999_latency_ms", +) + + +# Allowed CLOSED tunable parameters per Rules.md §5.6.4 table. +_CLOSED_ALLOWED_PARAMS = frozenset({ + # Database + "database.database", + # Index selection + "index.index_type", + "index.metric_type", + # DISKANN / HNSW / AISAQ build + search params (combined; submitter chooses one family) + "index.max_degree", + "index.search_list_size", + "index.M", + "index.ef_construction", + "index.inline_pq", + "search.search_ef", + # Run-time + "run.mode", + "run.num_query_processes", + "run.batch_size", + "run.report_count", + # Dataset / load + "dataset.collection_name", + "dataset.num_shards", + "dataset.chunk_size", + "dataset.batch_size", + "dataset.vector_dtype", + # Storage + "storage.storage_root", + "storage.storage_type", +}) - Mirrors the ``CheckpointingCheck`` / ``TrainingCheck`` constructor shape - (``__init__(self, log, config, submissions_logs)``) so the existing - ``for checker in checkers:`` loop in ``main.py`` (Plan 03-04) can - instantiate ``VdbCheck`` without any special-casing. - Emits zero violations. Future phase populates ``STUB_COVERAGE['VdbCheck']`` - in ``coverage_mapping.py`` when Rules.md §5 gains IDs. +# Additional OPEN params beyond the CLOSED set (Rules.md §5.6.5 table). +# Backend-specific params (pgvector lists/probes, Elasticsearch m / ef_construction +# / num_candidates, etc.) are NOT enumerable up-front; non-Milvus backends are +# handled via a warn-and-skip path below. +_OPEN_EXTRA_ALLOWED_PARAMS = frozenset({ + "database.host", + "database.port", +}) + + +class VdbCheck(BaseCheck): + """Check class for Rules.md §5 (Vector Database) rules. + + Mirrors the ``TrainingCheck`` / ``CheckpointingCheck`` constructor shape + (``__init__(self, log, config, submissions_logs)``) — Phase 3 D-S4 + invariant preserved so ``main.py`` instantiates every checker generically. """ def __init__(self, log, config: Config, submissions_logs: SubmissionLogs): @@ -40,28 +101,854 @@ def __init__(self, log, config: Config, submissions_logs: SubmissionLogs): Args: log: Logger instance (passed through to ``BaseCheck``). - config: A ``Config`` instance containing submission configuration. + config: A ``Config`` instance for submission configuration. submissions_logs: A ``SubmissionLogs`` instance for accessing - submission logs. The stub stores it but does not introspect - its contents. + submission logs. """ super().__init__(log=log, path=submissions_logs.loader_metadata.folder) self.config = config self.submissions_logs = submissions_logs + self.mode = self.submissions_logs.loader_metadata.mode + self.division = self.submissions_logs.loader_metadata.division self.name = "vdb checks" - self.checks = [] + self.run_path = os.path.join(self.path, "run") + self.datagen_path = os.path.join(self.path, "datagen") self.init_checks() def init_checks(self): - """Register the placeholder no-op (D-S2). + """Register all 16 §5 rule methods (Phase 4 D-01 full implementation).""" + self.checks = [ + self.vdb_dataset_scale, # 5.1.1 + self.vdb_dimension_consistency, # 5.1.2 + self.vdb_collection_populated, # 5.2.1 + self.vdb_index_build_completed, # 5.2.2 + self.vdb_run_count, # 5.3.1 + self.vdb_recall_reported, # 5.3.2 + self.vdb_query_count_minimum, # 5.3.3 + self.vdb_metrics_reported, # 5.3.4 + self.vdb_path_args, # 5.4.1 + self.vdb_filesystem_check, # 5.4.2 + self.vdb_object_storage_backend, # 5.5.1 + self.vdb_closed_submission_checksum, # 5.6.1 + self.vdb_closed_database_backend, # 5.6.2 + self.vdb_closed_index_types, # 5.6.3 + self.vdb_closed_submission_parameters, # 5.6.4 + self.vdb_open_submission_parameters, # 5.6.5 + ] + + # ----------------------------------------------------------------------- + # Helpers + # ----------------------------------------------------------------------- + + def _iter_run_files(self): + """Yield run-summary tuples or empty iterable when the loader did not populate them. + + Phase 4 land time: ``Loader.load()`` only fills ``run_files`` / + ``datagen_files`` for ``mode == "training"``; the ``else`` branch + fills ``checkpoint_files`` for everything else. For ``vector_database`` + leaves this means ``run_files`` is ``None`` (the dataclass default). + Rule methods consume this iterator instead of touching ``run_files`` + directly so they degrade to an empty walk without crashing. + """ + run_files = self.submissions_logs.run_files + if not run_files: + return iter(()) + return iter(run_files) + + def _iter_datagen_files(self): + """Counterpart to ``_iter_run_files`` for the datagen list.""" + datagen_files = self.submissions_logs.datagen_files + if not datagen_files: + return iter(()) + return iter(datagen_files) + + def _get_benchmark_api(self) -> str: + """Return 'file' or 'object' (default 'file') from the system YAML. + + Mirrors ``TrainingCheck._get_benchmark_api`` so 5.4.2 and 5.5.1 honor + the same per-API gating as the training filesystem check. + """ + system_file = getattr(self.submissions_logs, "system_file", None) + if not system_file: + return "file" + # `.get(key, {})` only catches missing keys — if the YAML serializes + # an intermediate node as `null`, the chained .get raises AttributeError + # on NoneType. `or {}` collapses both absent and null to a safe default. + sut = system_file.get("system_under_test") or {} + solution = sut.get("solution") or {} + architecture = solution.get("architecture") or {} + return architecture.get("benchmark_API", "file") + + def _vdb_loader_gap_warning(self, rule_id: str, rule_name: str) -> None: + """Emit a single warn_violation that the loader does not yet surface vector_database logs. + + This is the grep-visible signal required by the Phase-4 invariant + "must NEVER be a ``return True`` stub." When the loader gains a + vdb branch (loader.py 99-143), the run_files / datagen_files iters + become non-empty and these warnings drop out. + """ + self.warn_violation( + rule_id, rule_name, self.path, + "vector_database summary/metadata not surfaced by Loader at this revision; " + "rule structure is in place but cannot fire — gap tracked for the " + "loader vector_database branch follow-up", + ) + + # ----------------------------------------------------------------------- + # 5.1 Sizing + # ----------------------------------------------------------------------- + + @rule("5.1.1", "vdbDatasetScale") + def vdb_dataset_scale(self): + """Read num_vectors / dimension from each run's summary.json and compare + against a defined-scale table. (Rules.md 5.1.1) + + The scale-table constant is not yet defined in ``constants.py`` / + ``config.py`` — when it lands, replace the warn_violation with a + real lookup. Until then the rule is implemented and grep-visible + but does not fire. + """ + valid = True + if self.mode != "vector_database": + return valid + + # The defined-scale table is not yet in config.py; surface the gap. + self.warn_violation( + "5.1.1", "vdbDatasetScale", self.path, + "vdb scale table (num_vectors, dimension) not yet defined in " + "config.py; per-run scale check deferred", + ) + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if summary is None: + self.log.debug( + "[5.1.1] %s/%s: skipping (summary not loaded)", + self.path, ts, + ) + continue + num_vectors = summary.get("num_vectors") + dimension = summary.get("dimension") + if num_vectors is None: + self.log_violation( + "5.1.1", "vdbDatasetScale", self.path, + "summary.json at %s/%s is missing num_vectors", + self.path, ts, + ) + valid = False + if dimension is None: + self.log_violation( + "5.1.1", "vdbDatasetScale", self.path, + "summary.json at %s/%s is missing dimension", + self.path, ts, + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.1.1", "vdbDatasetScale") + + return valid - Rules.md §5 (VDB) is empty at Phase 3 land time. When that section - gains IDs, a future phase fills in real ``@rule``-decorated check - methods here and populates ``STUB_COVERAGE['VdbCheck']`` in - ``coverage_mapping.py``. + @rule("5.1.2", "vdbDimensionConsistency") + def vdb_dimension_consistency(self): + """Compare the load-time dimension against each run's dimension; mismatch fails. + (Rules.md 5.1.2) """ - self.checks = [self._section_unimplemented] + valid = True + if self.mode != "vector_database": + return valid + + load_dimensions = [] + for summary, metadata, ts in self._iter_datagen_files(): + if summary is None: + continue + dim = summary.get("dimension") + if dim is not None: + load_dimensions.append((dim, ts)) + + if not load_dimensions: + self.log.debug( + "[5.1.2] %s: no datagen summary surfaced; dimension cross-check skipped " + "(STRUCT-12/STRUCT-13 cover missing-datagen)", + self.path, + ) + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if summary is None: + continue + run_dim = summary.get("dimension") + if run_dim is None: + continue + for load_dim, load_ts in load_dimensions: + if load_dim != run_dim: + self.log_violation( + "5.1.2", "vdbDimensionConsistency", self.path, + "vdb dimension mismatch: datagen %s reports %s but run %s reports %s", + load_ts, load_dim, ts, run_dim, + ) + valid = False + + if not any_run and not load_dimensions: + self._vdb_loader_gap_warning("5.1.2", "vdbDimensionConsistency") + + return valid + + # ----------------------------------------------------------------------- + # 5.2 Generation + # ----------------------------------------------------------------------- + + @rule("5.2.1", "vdbCollectionPopulated") + def vdb_collection_populated(self): + """Confirm inserted_vectors >= num_vectors at load. (Rules.md 5.2.1)""" + valid = True + if self.mode != "vector_database": + return valid + + any_load = False + for summary, metadata, ts in self._iter_datagen_files(): + any_load = True + if summary is None: + continue + inserted = summary.get("inserted_vectors") + declared = summary.get("num_vectors") + if inserted is None or declared is None: + self.log_violation( + "5.2.1", "vdbCollectionPopulated", self.path, + "datagen summary at %s/%s missing inserted_vectors or num_vectors", + self.path, ts, + ) + valid = False + continue + try: + if int(inserted) < int(declared): + self.log_violation( + "5.2.1", "vdbCollectionPopulated", self.path, + "vdb collection underpopulated at %s/%s: " + "inserted %s of %s vectors at load time", + self.path, ts, inserted, declared, + ) + valid = False + except (TypeError, ValueError) as e: + self.log_violation( + "5.2.1", "vdbCollectionPopulated", self.path, + "datagen summary at %s/%s has non-numeric inserted/declared counts: %s", + self.path, ts, str(e), + ) + valid = False + + if not any_load: + self._vdb_loader_gap_warning("5.2.1", "vdbCollectionPopulated") + + return valid + + @rule("5.2.2", "vdbIndexBuildCompleted") + def vdb_index_build_completed(self): + """Confirm an index-build record is present in the load summary and that + the load-time index_type matches the run-time index_type. (Rules.md 5.2.2) + """ + valid = True + if self.mode != "vector_database": + return valid + + load_index_types = [] + any_load = False + for summary, metadata, ts in self._iter_datagen_files(): + any_load = True + if summary is None: + continue + idx_type = summary.get("index_type") + if idx_type is None: + self.log_violation( + "5.2.2", "vdbIndexBuildCompleted", self.path, + "datagen summary at %s/%s missing index_type " + "(no index-build record)", + self.path, ts, + ) + valid = False + continue + load_index_types.append((idx_type, ts)) + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if summary is None: + continue + run_idx = summary.get("index_type") + if run_idx is None: + continue + for load_idx, load_ts in load_index_types: + if load_idx != run_idx: + self.log_violation( + "5.2.2", "vdbIndexBuildCompleted", self.path, + "vdb index_type changed between datagen %s (%s) and run %s (%s)", + load_ts, load_idx, ts, run_idx, + ) + valid = False + + if not any_load and not any_run: + self._vdb_loader_gap_warning("5.2.2", "vdbIndexBuildCompleted") + + return valid + + # ----------------------------------------------------------------------- + # 5.3 Run + # ----------------------------------------------------------------------- + + @rule("5.3.1", "vdbRunCount") + def vdb_run_count(self): + """Verify exactly five timestamp directories under /run/. + (Rules.md 5.3.1; Phase 4 D-04: count applies to run/, not datagen/.) + """ + valid = True + if self.mode != "vector_database": + return valid + + # STRUCT layer owns missing-run/ structural violation. + if not os.path.isdir(self.run_path): + return valid + + timestamps = [ + d for d in os.listdir(self.run_path) + if os.path.isdir(os.path.join(self.run_path, d)) and not d.startswith(".") + ] + if len(timestamps) != 5: + self.log_violation( + "5.3.1", "vdbRunCount", self.run_path, + "vdbRunCount: expected exactly 5 run timestamp directories under %s, found %d", + self.run_path, len(timestamps), + ) + valid = False + + return valid + + @rule("5.3.2", "vdbRecallReported") + def vdb_recall_reported(self): + """Verify a recall value is present in summary.json or recall_stats.json + for each run and that it meets the minimum target for the scale. + (Rules.md 5.3.2) + + The minimum-target table per scale/metric is not yet in config.py; + the presence check still runs, the threshold check is deferred via + warn_violation. + """ + valid = True + if self.mode != "vector_database": + return valid + + # The minimum-recall target table is not yet in config.py. + self.warn_violation( + "5.3.2", "vdbRecallReported", self.path, + "vdb minimum-recall target table (per scale/metric) not yet " + "defined in config.py; threshold check deferred — presence " + "check still runs", + ) + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if summary is None: + self.log.debug( + "[5.3.2] %s/%s: skipping (summary not loaded)", + self.path, ts, + ) + continue + recall = summary.get("recall") + if recall is None: + # Fall back to rank-local recall_stats.json adjacent to summary.json + recall_stats_path = os.path.join(self.run_path, ts, "recall_stats.json") + if not os.path.isfile(recall_stats_path): + self.log_violation( + "5.3.2", "vdbRecallReported", self.path, + "vdbRecallReported: no recall value present in " + "summary.json or recall_stats.json at %s/%s", + self.path, ts, + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.3.2", "vdbRecallReported") + + return valid + + @rule("5.3.3", "vdbQueryCountMinimum") + def vdb_query_count_minimum(self): + """Verify each run issued at least the minimum number of queries. + (Rules.md 5.3.3) + + The minimum-query table per scale is not yet in config.py; structure + is in place, threshold check deferred via warn_violation. + """ + valid = True + if self.mode != "vector_database": + return valid + + self.warn_violation( + "5.3.3", "vdbQueryCountMinimum", self.path, + "vdb minimum-query target table (per scale) not yet defined in " + "config.py; threshold check deferred — presence check still runs", + ) + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if summary is None: + continue + qps = summary.get("throughput_qps") + total_time = summary.get("total_time_seconds") + query_count = summary.get("query_count") + if qps is None and query_count is None: + self.log_violation( + "5.3.3", "vdbQueryCountMinimum", self.path, + "vdbQueryCountMinimum: summary.json at %s/%s has neither " + "throughput_qps nor query_count — cannot compute issued queries", + self.path, ts, + ) + valid = False + elif query_count is None and total_time is None: + self.log_violation( + "5.3.3", "vdbQueryCountMinimum", self.path, + "vdbQueryCountMinimum: summary.json at %s/%s missing total_time_seconds " + "for QPS-based issued-query computation", + self.path, ts, + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.3.3", "vdbQueryCountMinimum") + + return valid + + @rule("5.3.4", "vdbMetricsReported") + def vdb_metrics_reported(self): + """Verify each run's summary.json reports the required metric fields. + (Rules.md 5.3.4) + """ + valid = True + if self.mode != "vector_database": + return valid + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if summary is None: + continue + for field in _REQUIRED_METRIC_FIELDS: + if field not in summary: + self.log_violation( + "5.3.4", "vdbMetricsReported", self.path, + "vdbMetricsReported: required field %r missing from summary.json at %s/%s", + field, self.path, ts, + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.3.4", "vdbMetricsReported") + + return valid + + # ----------------------------------------------------------------------- + # 5.4 POSIX-API options + # ----------------------------------------------------------------------- + + @rule("5.4.1", "vdbPathArgs") + def vdb_path_args(self): + """Verify vdb data path and results dir args are both set and differ. + (Rules.md 5.4.1) + """ + valid = True + if self.mode != "vector_database": + return valid + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if metadata is None: + self.log.debug( + "[5.4.1] %s/%s: skipping (metadata not loaded)", + self.path, ts, + ) + continue + args = metadata.get("args", {}) + # The vdb runner uses storage-root for the data path; data_dir is the + # generic mlpstorage name. Honor either to keep the rule resilient + # to the args-shape refactor that lands alongside Phase 4. + data_path = ( + args.get("storage_root") + or args.get("data_dir") + or args.get("vdb_data_path") + ) + results_dir = args.get("results_dir") + + if not data_path: + self.log_violation( + "5.4.1", "vdbPathArgs", self.path, + "vdbPathArgs: vdb data path arg not set in metadata at %s/%s", + self.path, ts, + ) + valid = False + if not results_dir: + self.log_violation( + "5.4.1", "vdbPathArgs", self.path, + "vdbPathArgs: results_dir not set in metadata at %s/%s", + self.path, ts, + ) + valid = False + if data_path and results_dir and data_path == results_dir: + self.log_violation( + "5.4.1", "vdbPathArgs", self.path, + "vdbPathArgs: vdb data path %s and results_dir %s must differ", + data_path, results_dir, + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.4.1", "vdbPathArgs") + + return valid + + @rule("5.4.2", "vdbFilesystemCheck") + def vdb_filesystem_check(self): + """Verify vdb data dir and results dir are on different filesystems. + (Rules.md 5.4.2) + + Reuses the canonical ``_check_filesystem_separation`` helper that + TrainingCheck.3.4.2 / CheckpointingCheck.4.4.2 use. Object-API + submissions silent-pass (D-B7). + """ + valid = True + if self.mode != "vector_database": + return valid + + if self._get_benchmark_api() == "object": + return valid + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if metadata is None: + self.log.debug( + "[5.4.2] %s/%s: skipping (metadata not loaded)", + self.path, ts, + ) + continue + args = metadata.get("args", {}) or {} + # _check_filesystem_separation looks up "data_dir" or + # "checkpoint_folder"; for vdb the analog is storage_root. Synthesize + # a flat dict so the helper sees data_dir + results_dir. + shim_args = dict(args) + if "data_dir" not in shim_args: + storage_root = args.get("storage_root") or args.get("vdb_data_path") + if storage_root: + shim_args["data_dir"] = storage_root + logfile_path = os.path.join(self.run_path, ts, "vdb_run.stdout.log") + ok, df_found = _check_filesystem_separation(shim_args, logfile_path) + if not df_found: + self.log_violation( + "5.4.2", "vdbFilesystemCheck", logfile_path, + "df output not found", + ) + valid = False + continue + if not ok: + self.log_violation( + "5.4.2", "vdbFilesystemCheck", logfile_path, + "vdbFilesystemCheck: vdb data path and results_dir are on the " + "same filesystem", + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.4.2", "vdbFilesystemCheck") + + return valid + + # ----------------------------------------------------------------------- + # 5.5 Object-API options + # ----------------------------------------------------------------------- + + @rule("5.5.1", "vdbObjectStorageBackend") + def vdb_object_storage_backend(self): + """For object-API submissions, verify the storage backend is S3-compatible + and consistent with the declared API. (Rules.md 5.5.1) + """ + valid = True + if self.mode != "vector_database": + return valid + + # Only applies under object API. + if self._get_benchmark_api() != "object": + return valid + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if summary is None: + continue + backend = ( + summary.get("database", {}).get("storage_backend") + if isinstance(summary.get("database"), dict) + else None + ) + if not backend: + self.log_violation( + "5.5.1", "vdbObjectStorageBackend", self.path, + "vdbObjectStorageBackend: object-API submission missing " + "database.storage_backend in summary.json at %s/%s", + self.path, ts, + ) + valid = False + continue + # S3-compatible backends: accept exact names or `s3-` prefix + # (e.g. "s3-compatible", "s3-express"). Substring match is too + # loose — "non-s3-storage" / "s3-incompatible-fork" should fail. + backend_lc = str(backend).lower() + _S3_COMPATIBLE_NAMES = frozenset({"s3", "s3-compatible", "minio", "ceph"}) + _S3_COMPATIBLE_PREFIXES = ("s3-",) + if ( + backend_lc not in _S3_COMPATIBLE_NAMES + and not backend_lc.startswith(_S3_COMPATIBLE_PREFIXES) + ): + self.log_violation( + "5.5.1", "vdbObjectStorageBackend", self.path, + "vdbObjectStorageBackend: object-API submission must record an " + "S3-compatible backend; found %r", + backend, + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.5.1", "vdbObjectStorageBackend") + + return valid + + # ----------------------------------------------------------------------- + # 5.6 OPEN vs CLOSED + # ----------------------------------------------------------------------- + + @rule("5.6.1", "vdbClosedSubmissionChecksum") + def vdb_closed_submission_checksum(self): + """For CLOSED submissions, verify the code-image self-consistency + + upstream-identity via the shared layered helper. (Rules.md 5.6.1) + + Phase 4 D-06 / CD-04: delegates to + ``helpers._check_code_image_layered`` — the SAME helper + ``TrainingCheck.3.6.1`` calls — so the layered model is implemented + once and attributed under the caller's rule ID/name. + + Walk-up: ``self.path`` is the per-leaf vdb path + (``/closed//results//vector_database/``). + The CLOSED ``code/`` lives at ``/closed//code/``, + four levels above ``self.path`` (DisplayIndex → vector_database → system + → results → ````). + + Missing ``code/`` is NOT logged here — STRUCT-06 (§2.1.6) owns the + VALS-01 missing-code/ violation; re-firing here would double-count. + """ + if self.mode != "vector_database": + return True + if self.division != "closed": + return True + + # /closed//results//vector_database/ + # walk up four levels: DisplayIndex → vector_database → system → results → + submitter_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(self.path)))) + code_path = os.path.join(submitter_path, "code") + + if not os.path.isdir(code_path): + return True # STRUCT-06 owns missing-code/ + + expected = self.config.get_reference_checksum() + return _check_code_image_layered( + code_path, + "closed", + expected, + self.log, + self.log_violation, + "5.6.1", + "vdbClosedSubmissionChecksum", + ) + + @rule("5.6.2", "vdbClosedDatabaseBackend") + def vdb_closed_database_backend(self): + """For CLOSED, verify database.database == 'milvus'. (Rules.md 5.6.2)""" + valid = True + if self.mode != "vector_database": + return valid + if self.division != "closed": + return valid + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if summary is None: + continue + db_block = summary.get("database") + backend = db_block.get("database") if isinstance(db_block, dict) else None + if backend != "milvus": + self.log_violation( + "5.6.2", "vdbClosedDatabaseBackend", self.path, + "vdbClosedDatabaseBackend: CLOSED requires milvus backend, " + "found %r at %s/%s", + backend, self.path, ts, + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.6.2", "vdbClosedDatabaseBackend") + + return valid + + @rule("5.6.3", "vdbClosedIndexTypes") + def vdb_closed_index_types(self): + """For CLOSED, verify index type is DISKANN / HNSW / AISAQ and that + the on-disk directory name matches the summary.json index_type. + (Rules.md 5.6.3.) + """ + valid = True + if self.mode != "vector_database": + return valid + if self.division != "closed": + return valid + + # On-disk directory name is the UPPERCASE token; compare directly. + dir_name = os.path.basename(self.path.rstrip(os.sep)) + if dir_name not in VDB_INDEX_TYPES_CLOSED: + self.log_violation( + "5.6.3", "vdbClosedIndexTypes", self.path, + "vdbClosedIndexTypes: directory name %r is not a CLOSED index " + "type (allowed: %s)", + dir_name, list(VDB_INDEX_TYPES_CLOSED), + ) + valid = False + return valid + token = dir_name + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if summary is None: + continue + run_idx = summary.get("index_type") + if run_idx is None: + self.log_violation( + "5.6.3", "vdbClosedIndexTypes", self.path, + "vdbClosedIndexTypes: summary.json at %s/%s missing index_type", + self.path, ts, + ) + valid = False + continue + if run_idx != token: + self.log_violation( + "5.6.3", "vdbClosedIndexTypes", self.path, + "vdbClosedIndexTypes: directory %r expects index_type %r " + "but summary.json reports %r", + dir_name, token, run_idx, + ) + valid = False + + if not any_run: + # On-disk check has already run; loader gap only affects the per-run + # comparison. Surface the gap so the rule's grep-visible signal is + # consistent with the rest. + self._vdb_loader_gap_warning("5.6.3", "vdbClosedIndexTypes") + + return valid + + @rule("5.6.4", "vdbClosedSubmissionParameters") + def vdb_closed_submission_parameters(self): + """For CLOSED, verify only allowed parameters are modified. + (Rules.md 5.6.4) + """ + valid = True + if self.mode != "vector_database": + return valid + if self.division != "closed": + return valid + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if metadata is None: + self.log.debug( + "[5.6.4] %s/%s: skipping (metadata not loaded)", + self.path, ts, + ) + continue + params_dict = metadata.get("params_dict", {}) or {} + for param_key in params_dict.keys(): + if param_key not in _CLOSED_ALLOWED_PARAMS: + self.log_violation( + "5.6.4", "vdbClosedSubmissionParameters", self.path, + "CLOSED vdb submission modifies disallowed parameter: %s", + param_key, + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.6.4", "vdbClosedSubmissionParameters") + + return valid + + @rule("5.6.5", "vdbOpenSubmissionParameters") + def vdb_open_submission_parameters(self): + """For OPEN, verify only allowed parameters are modified. + (Rules.md 5.6.5) + + OPEN extends the CLOSED allowlist with database.host / database.port. + Backend-specific parameters for non-Milvus backends are NOT + enumerable up-front; for those backends the strict allowlist is + relaxed and a single warn_violation is emitted per leaf so the + relaxation is grep-visible. + """ + valid = True + if self.mode != "vector_database": + return valid + if self.division != "open": + return valid + + allowed_params = _CLOSED_ALLOWED_PARAMS | _OPEN_EXTRA_ALLOWED_PARAMS + + any_run = False + for summary, metadata, ts in self._iter_run_files(): + any_run = True + if metadata is None: + self.log.debug( + "[5.6.5] %s/%s: skipping (metadata not loaded)", + self.path, ts, + ) + continue + # Determine backend from this run's summary so non-Milvus + # backends are exempted from the strict allowlist (Rules.md + # §5.6.5: "any index types, metrics, and parameters native to + # a non-Milvus backend"). + backend = None + if summary is not None: + db_block = summary.get("database") + if isinstance(db_block, dict): + backend = db_block.get("database") + if backend not in (None, "milvus"): + self.warn_violation( + "5.6.5", "vdbOpenSubmissionParameters", self.path, + "OPEN vdb submission uses non-Milvus backend %r at %s/%s; " + "backend-specific parameter validation is permitted but " + "not enforced — strict allowlist relaxed for this leaf", + backend, self.path, ts, + ) + continue + params_dict = metadata.get("params_dict", {}) or {} + for param_key in params_dict.keys(): + if param_key not in allowed_params: + self.log_violation( + "5.6.5", "vdbOpenSubmissionParameters", self.path, + "OPEN vdb submission modifies disallowed parameter: %s", + param_key, + ) + valid = False + + if not any_run: + self._vdb_loader_gap_warning("5.6.5", "vdbOpenSubmissionParameters") - def _section_unimplemented(self) -> bool: - """No-op placeholder. Emits zero violations (Phase 3 success criterion #2).""" - return True + return valid diff --git a/mlpstorage_py/submission_checker/constants.py b/mlpstorage_py/submission_checker/constants.py index 60f51ab4..bab66407 100644 --- a/mlpstorage_py/submission_checker/constants.py +++ b/mlpstorage_py/submission_checker/constants.py @@ -154,8 +154,22 @@ def _derive_default_spec_version(package_version: str, supported: list) -> str: # Directory-name prefixes excluded from the code-tree MD5 (Rules.md 2.1.6). # Match is against POSIX-joined relative paths with a trailing slash so that # `.gitignore` (file) does not collide with `.git/` (directory prefix). +# +# Dot-prefixed entries (.git/, .idea/, .planning/, etc.) catch local +# developer / tooling artifacts that the project's .gitignore already +# excludes from version control. They are not part of the benchmark +# source contract, would change every time a contributor's tools change, +# and (in the .gsd-tmp/ case) would even contain transient agent state. MD5_EXCLUDE_PREFIXES: tuple[str, ...] = ( ".git/", + ".idea/", # JetBrains IDE workspace + ".vscode/", # VS Code workspace + ".claude/", # Claude CLI runtime / settings + ".agent/", # Agent runtime (per project .gitignore "Coding Agents") + ".agents/", # Same, alternate name + ".roo/", # Roo agent runtime + ".planning/", # GSD planning artifacts (project-local) + ".gsd-tmp/", # GSD code-fixer worktree (project-local) "__pycache__/", ".pytest_cache/", ".venv/", @@ -163,12 +177,15 @@ def _derive_default_spec_version(package_version: str, supported: list) -> str: "build/", "dist/", ".tox/", + "test/", + "tests/", ) # Filename patterns excluded from the code-tree MD5 (Rules.md 2.1.6). # Matched against the basename. ``.egg-info`` is handled at the prefix level # (any directory ending in ``.egg-info``) — keep that in the predicate, not here. MD5_EXCLUDE_FILENAMES: tuple[str, ...] = ( + ".code-hash.json", "*.pyc", "*.pyo", ".DS_Store", diff --git a/mlpstorage_py/submission_checker/coverage_mapping.py b/mlpstorage_py/submission_checker/coverage_mapping.py index ff7689d1..fbd5dfea 100644 --- a/mlpstorage_py/submission_checker/coverage_mapping.py +++ b/mlpstorage_py/submission_checker/coverage_mapping.py @@ -32,10 +32,12 @@ OUT_OF_SCOPE_RULES: dict[str, str] = {} -# D-A1/D-S3: lists are empty because Rules.md §5 (VDB) and §6 (KVCache) -# are empty at Phase 3 land time. Populate the lists when those sections -# gain IDs (e.g., ``"VdbCheck": ["5.1.1", "5.1.2"]``). +# Stub-class coverage advertisement: maps stub class name -> list of Rules.md +# rule IDs the stub stands in for. VdbCheck used to live here when Rules.md +# §5 was empty; after Phase 4 Plan 04-02 (D-01) it carries real +# ``@rule``-decorated methods for every §5 ID (5.1.1-5.6.5) and +# ``discover_rules`` picks them up directly, so the VdbCheck entry has been +# removed. KVCacheCheck stays until Rules.md §6 (KVCache) gains IDs. STUB_COVERAGE: dict[str, list[str]] = { - "VdbCheck": [], # populated when Rules.md §5 (VDB) gains IDs "KVCacheCheck": [], # populated when Rules.md §6 (KVCache) gains IDs } diff --git a/mlpstorage_py/submission_checker/tools/code_checksum.py b/mlpstorage_py/submission_checker/tools/code_checksum.py index 6fd07226..47e26a72 100644 --- a/mlpstorage_py/submission_checker/tools/code_checksum.py +++ b/mlpstorage_py/submission_checker/tools/code_checksum.py @@ -115,8 +115,15 @@ def compute_code_tree_md5(root_path: str, log) -> str | None: def _is_excluded_dir(dirpath: str, dirname: str, root_path: str) -> bool: """Return True if ``dirname`` inside ``dirpath`` should be pruned. - Checks both the MD5_EXCLUDE_PREFIXES list (path-prefix match) and the - ``.egg-info`` suffix (D-13: handled in predicate, not in constant). + Matches against MD5_EXCLUDE_PREFIXES by basename at any depth (so + ``__pycache__/``, ``tests/``, ``build/``, etc. are pruned wherever they + appear in the tree, not only at the root). Also matches by rooted + relative path for completeness, and excludes any ``.egg-info`` directory + (D-13: handled in predicate, not in the constant). + + The basename-at-any-depth semantics intentionally mirror + ``capture_code_image``'s ``shutil.copytree`` ignore callback so the two + walkers produce identical hashes for the same logical tree. Args: dirpath: Absolute path of the parent directory (from os.walk). @@ -126,6 +133,12 @@ def _is_excluded_dir(dirpath: str, dirname: str, root_path: str) -> bool: Returns: True if the directory should be excluded from the walk. """ + # Basename-at-any-depth: ``__pycache__/leak.txt`` should be pruned whether + # __pycache__/ sits at the root or deeply nested inside a package. + for prefix in MD5_EXCLUDE_PREFIXES: + if dirname == prefix.rstrip("/"): + return True + full_dir = os.path.join(dirpath, dirname) rel_dir = os.path.relpath(full_dir, root_path).replace(os.sep, "/") + "/" diff --git a/mlpstorage_py/submission_checker/tools/code_image.py b/mlpstorage_py/submission_checker/tools/code_image.py new file mode 100644 index 00000000..b478ea43 --- /dev/null +++ b/mlpstorage_py/submission_checker/tools/code_image.py @@ -0,0 +1,717 @@ +"""Code-image capture, load, and verification tooling. + +Implements the capture-at-runtime and integrity-verification semantics +specified in Phase 1 and 2 of the MLPerf Storage Code-Image initiative. + +Design decisions (D-01..D-20): +- D-01: Module lives at mlpstorage_py/submission_checker/tools/code_image.py. +- D-02: Public API: capture_code_image, load_code_image, verify_source_against_image, + verify_image_self_consistent, find_source_root; CodeImage dataclass. +- D-03: Typed CodeImageError hierarchy for CLI mapping. +- D-04: find_source_root ascends to pyproject.toml. +- D-05: SourceRootNotFound raised at filesystem root. +- D-07: .code-hash.json schema (hash, algorithm, captured_at, mlpstorage_version, git_sha). +- D-08: git_sha captured via best-effort 'git rev-parse HEAD'. +- D-09: algorithm identifier 'md5-tree-v1' is stable. +- D-10: captured_at in canonical ISO-8601 UTC 'Z' form. +- D-11: Runtime check hashes live source against captured image. +- D-12: Submission check hashes captured tree against its own JSON. +- D-14: Missing JSON in existing code/ is a fatal error. +- D-15: Malformed JSON is a fatal error. +- D-16: Never silently re-capture an existing code/ image. +- D-17: Atomic capture via code.tmp/ then os.rename. +- D-18: Cleanup stale code.tmp/ before starting capture. +- D-19: JSON hash is computed from the captured copy, not live source. + +Public API: + find_source_root(start=None) -> Path + capture_code_image(source_root, target_dir, log) -> CodeImage + load_code_image(image_dir, log) -> CodeImage + verify_source_against_image(source_root, image_dir, log) -> bool + verify_image_self_consistent(image_dir, log) -> bool + CodeImage (dataclass) + CodeImageError (Exception) +""" + +import datetime +import fnmatch +import json +import os +import re +import shutil +import subprocess +from dataclasses import dataclass +from pathlib import Path + +from mlpstorage_py import __version__ as MLPSTORAGE_VERSION +from mlpstorage_py.config import BENCHMARK_TYPES +from mlpstorage_py.errors import ConfigurationError, ErrorCode +from mlpstorage_py.rules.utils import ( + MLPSTORAGE_ORGNAME_ENVVAR, + MLPSTORAGE_SYSTEMNAME_ENVVAR, +) +from .code_checksum import compute_code_tree_md5 +from ..constants import MD5_EXCLUDE_FILENAMES, MD5_EXCLUDE_PREFIXES + + +# CLI subparser name → canonical on-disk type segment. +# generate_output_location() writes this same segment, so the captured code/ +# must use it to live in the same submission tree. CLI names map to the +# BENCHMARK_TYPES enum value, whose .name is used as the on-disk segment for +# all four types. +_CLI_BENCHMARK_TO_TYPE: dict[str, BENCHMARK_TYPES] = { + "training": BENCHMARK_TYPES.training, + "checkpointing": BENCHMARK_TYPES.checkpointing, + "vectordb": BENCHMARK_TYPES.vector_database, + "kvcache": BENCHMARK_TYPES.kv_cache, +} + +# On-disk type segment is the BENCHMARK_TYPES.name for every benchmark type. +_TYPE_TO_ONDISK_SEGMENT: dict[BENCHMARK_TYPES, str] = { + BENCHMARK_TYPES.training: BENCHMARK_TYPES.training.name, + BENCHMARK_TYPES.checkpointing: BENCHMARK_TYPES.checkpointing.name, + BENCHMARK_TYPES.vector_database: BENCHMARK_TYPES.vector_database.name, + BENCHMARK_TYPES.kv_cache: BENCHMARK_TYPES.kv_cache.name, +} + +# Per-type "leaf attribute" on args. The OPEN capture/verify path includes +# this segment between / and code/ so each leaf — what the submitter +# would consider a single comparable result group — has its own code image. +# +# training, checkpointing : per- → uses args.model +# vector_database : per- → uses args.index_type +# (AISAQ results are not comparable to DISKANN +# or HNSW, so they live in separate trees). +# The index name is UPPERCASE on disk, matching +# args.index_type and summary.json.index_type. +# kv_cache : transitional — → None (no leaf segment) +# code lives at /code/ until the kv_cache +# directory/file structure below the prefix is +# finalized (per follow-up plan). +# +# None means "no leaf segment" — code is captured per benchmark type only. +_TYPE_TO_LEAF_ATTR: dict[BENCHMARK_TYPES, str | None] = { + BENCHMARK_TYPES.training: "model", + BENCHMARK_TYPES.checkpointing: "model", + BENCHMARK_TYPES.vector_database: "index_type", + BENCHMARK_TYPES.kv_cache: None, +} + + +class CodeImageError(Exception): + """Base for all code-image capture/verify failures (D-03).""" + + +class MissingHashFile(CodeImageError): + """.code-hash.json not found in an image directory (D-14).""" + + +class MalformedHashFile(CodeImageError): + """.code-hash.json present but unparseable or invalid (D-15).""" + + +class SourceRootNotFound(CodeImageError): + """find_source_root walked to filesystem root without finding pyproject.toml (D-05).""" + + +class CodeTreeUnreadable(CodeImageError): + """compute_code_tree_md5 returned None for a tree that should be readable. + + Raised when a code/ or source tree exists but the hashing walk could not + complete — e.g., a permission error mid-walk, or a path that is gone by + the time the walk reaches it. Distinct from MissingHashFile (the + `.code-hash.json` sidecar is missing) and SourceRootNotFound (no + pyproject.toml ancestor) so the caller can log the right diagnostic. + """ + + +@dataclass(frozen=True) +class CodeImage: + """In-memory representation of a captured code image (D-02).""" + path: Path + hash: str + algorithm: str + captured_at: str + mlpstorage_version: str + git_sha: str | None + + +# Private constants +_HASH_FILENAME = ".code-hash.json" +_TMP_SUFFIX = "code.tmp" +_CODE_DIRNAME = "code" +_ALGORITHM = "md5-tree-v1" +_GIT_TIMEOUT_SEC = 5 +_HASH_HEX_LEN = 32 +_GIT_SHA_LEN = 40 + +# POSIX-safe name pattern per Rules.md §2.1.1 + path-traversal guard for `.` / `..` +# (D-05; T-02-02-05 mitigation made INLINE per Gemini + plan-checker consensus, REVIEWS.md): +_SUBMITTER_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$") +# The regex above MATCHES the literal strings "." and "..". An additional explicit +# reject is required to prevent path-traversal exploits (Gemini + plan-checker +# consensus, REVIEWS.md). This is checked INLINE in capture_or_verify_code_image, +# not deferred to a follow-up. +_RESERVED_PATH_SEGMENTS = frozenset({".", ".."}) + +# Submission-mode gating sets (D-10). +_SUBMISSION_MODES = frozenset({"closed", "open"}) +_SUBMISSION_COMMANDS = frozenset({"datasize", "datagen", "run"}) + + +def find_source_root(start: Path | None = None) -> Path: + """Ascend from start until a directory with pyproject.toml is found (D-04). + + Args: + start: Directory to start searching from. Defaults to the directory + containing this file. + + Returns: + Absolute Path to the repository root. + + Raises: + SourceRootNotFound: If the walk reaches the filesystem root. + """ + curr = (start or Path(__file__)).resolve() + if curr.is_file(): + curr = curr.parent + + while True: + if (curr / "pyproject.toml").exists(): + return curr + if curr.parent == curr: # reached root + break + curr = curr.parent + + raise SourceRootNotFound( + f"Could not find source root (pyproject.toml) ascending from {start or Path(__file__)}" + ) + + +def capture_code_image(source_root: Path, target_dir: Path, log) -> CodeImage: + """Capture a frozen copy of source_root into target_dir/code/ (D-02, CAP-01/02). + + 1. Removes any stale 'code.tmp/' in target_dir (D-18). + 2. Copies source_root into 'code.tmp/' minus exclusions (CAP-03/04). + 3. Hashes the captured copy (D-19, HASH-01). + 4. Writes .code-hash.json into 'code.tmp/' (CAP-05). + 5. Atomically renames 'code.tmp/' to 'code/' (D-17). + + Args: + source_root: Root of the benchmark source tree. + target_dir: Directory where the 'code/' subdirectory will be created. + log: Logger object. + + Returns: + A CodeImage instance representing the new capture. + + Raises: + ConfigurationError: If MLPSTORAGE_VERSION resolved to the literal + "unknown" sentinel (no installed dist metadata and no readable + pyproject.toml) — refusing to stamp a degenerate version into + .code-hash.json that would degrade submission-time forensics. + CodeImageError: If target_dir/code/ already exists (D-16). + SourceRootNotFound: If source_root is missing or hashing fails. + """ + # Refuse to capture with a degenerate mlpstorage_version sentinel — fail + # before any filesystem work so we leave no partial state behind. + if MLPSTORAGE_VERSION == "unknown": + raise ConfigurationError( + "mlpstorage version could not be resolved (no installed distribution " + "metadata and no readable pyproject.toml); refusing to capture with " + "mlpstorage_version=\"unknown\" — install the package " + "(pip install -e . / uv sync) or run from a checkout with pyproject.toml", + code=ErrorCode.CONFIG_MISSING_REQUIRED, + ) + + code_dir = target_dir / _CODE_DIRNAME + code_tmp = target_dir / _TMP_SUFFIX + + if code_dir.exists(): + raise CodeImageError(f"Code image already exists at {code_dir} (D-16)") + + if code_tmp.exists(): + log.warning("stale code.tmp/ at %s removed before capture (D-18)", code_tmp) + shutil.rmtree(code_tmp) + + # Behavior 5: Exclusion delegated to identical logic as hash + _atomic_capture(source_root, code_tmp, log) + + # D-17 atomicity contract: code.tmp/ must be removed on ANY failure + # between copy and rename — otherwise the next attempt finds a stale + # tmp tree and only logs a warning. Wrap hash + JSON-write + rename in + # try/except BaseException so KeyboardInterrupt / SystemExit also clean up. + try: + # Behavior 3/4: Hash the captured copy + digest = compute_code_tree_md5(str(code_tmp), log) + if digest is None: + # This shouldn't happen if _atomic_capture succeeded, but for safety: + raise SourceRootNotFound(f"Failed to hash captured tree at {code_tmp}") + + # Behavior 6: Build payload + payload = { + "hash": digest, + "algorithm": _ALGORITHM, + "captured_at": _now_utc_iso(), + "mlpstorage_version": MLPSTORAGE_VERSION, + "git_sha": _resolve_git_sha(source_root, log), + } + + # Behavior 6: Write JSON + _write_hash_file(code_tmp, payload, log) + + # Behavior 4: Atomic rename + os.rename(str(code_tmp), str(code_dir)) + except BaseException: + if code_tmp.exists(): + shutil.rmtree(code_tmp, ignore_errors=True) + raise + + return CodeImage(path=code_dir, **payload) + + +def load_code_image(image_dir: Path, log) -> CodeImage: + """Read and validate .code-hash.json from an image directory (D-02, D-14, D-15). + + Args: + image_dir: Path to the 'code/' directory. + log: Logger object. + + Returns: + CodeImage instance. + + Raises: + MissingHashFile: If .code-hash.json is absent. + MalformedHashFile: If JSON is invalid or missing required fields. + """ + data = _read_hash_file(image_dir, log) + return CodeImage(path=image_dir, **data) + + +def verify_source_against_image(source_root: Path, image_dir: Path, log) -> bool: + """Compare live source tree against a captured image (D-11, VALR-01..04). + + Args: + source_root: Path to the running benchmark source. + image_dir: Path to the captured 'code/' directory. + log: Logger object. + + Returns: + True if hashes match, False otherwise. + + Raises: + CodeTreeUnreadable: If source_root exists but the hashing walk could + not complete (permission error mid-walk, etc.). + MissingHashFile / MalformedHashFile: If image_dir is missing or has + an invalid `.code-hash.json` (via load_code_image). + """ + img = load_code_image(image_dir, log) + current_hash = compute_code_tree_md5(str(source_root), log) + if current_hash is None: + # IN-02: previously raised SourceRootNotFound, but that exception is + # reserved for "walked to filesystem root without finding pyproject.toml" + # (D-05) — a structural CLI / config error. compute_code_tree_md5 + # returning None means the walk itself failed, not that source_root + # is structurally invalid. Use CodeTreeUnreadable instead. + raise CodeTreeUnreadable( + f"Source root could not be hashed (unreadable or vanished mid-walk): {source_root}" + ) + + return current_hash == img.hash + + +def verify_image_self_consistent(image_dir: Path, log) -> bool: + """Verify that a captured 'code/' tree matches its own recorded hash (D-12, VALS-02/04). + + Used by the submission validator to detect post-capture tampering. + + Args: + image_dir: Path to the captured 'code/' directory. + log: Logger object. + + Returns: + True if the tree hash matches .code-hash.json, False otherwise. + + Raises: + MissingHashFile: If .code-hash.json is absent (via load_code_image). + MalformedHashFile: If .code-hash.json is unparseable (via load_code_image). + CodeTreeUnreadable: If the image_dir tree itself cannot be hashed + (permission error mid-walk, gone by the time we walk, etc.). + """ + img = load_code_image(image_dir, log) + actual_hash = compute_code_tree_md5(str(image_dir), log) + if actual_hash is None: + # IN-01: previously raised MissingHashFile here, but load_code_image + # already succeeded — the JSON IS present. The real failure is that + # the tree itself didn't hash. Use CodeTreeUnreadable so the log + # message names the actual root cause. + raise CodeTreeUnreadable( + f"Captured code directory is missing or unreadable: {image_dir}" + ) + + return actual_hash == img.hash + + +# --------------------------------------------------------------------------- +# Private Helpers +# --------------------------------------------------------------------------- + +def _atomic_capture(source_root: Path, target_dir: Path, log) -> None: + """Copy source_root to target_dir using identical exclusion logic as hashing (Behavior 5).""" + source_str = str(source_root) + # shutil.copytree(..., dirs_exist_ok=True) below creates target_dir on its + # own (Python ≥3.8). No need to pre-mkdir — keeping the call shrinks the + # window in which target_dir can be in a partial state when copytree starts. + + # We use shutil.copytree with a custom ignore function to replicate the + # predicate's exclusion logic exactly. + def ignore_logic(curr_dir, contents): + ignored = set() + # Rel_dir from source_root for prefix matching + rel_dir = os.path.relpath(curr_dir, source_str).replace(os.sep, "/") + if rel_dir == ".": + rel_dir = "" + else: + rel_dir += "/" + + for name in contents: + full_item = os.path.join(curr_dir, name) + rel_item = rel_dir + name + + # 1. Directory exclusion (Prefixes or .egg-info) + if os.path.isdir(full_item): + # Match if basename is in prefixes (stripped) OR if rel_path starts with prefix + item_prefix = rel_item + "/" + if any(item_prefix.startswith(p) for p in MD5_EXCLUDE_PREFIXES) or \ + any(name == p.rstrip("/") for p in MD5_EXCLUDE_PREFIXES): + ignored.add(name) + continue + # .egg-info handled specially in predicate + if name.endswith(".egg-info"): + ignored.add(name) + continue + else: + # 2. Filename-based exclusion + if any(fnmatch.fnmatch(name, pat) for pat in MD5_EXCLUDE_FILENAMES): + ignored.add(name) + continue + # 3. Symlinks (hash skips them, so capture must skip them to stay consistent) + if os.path.islink(full_item): + ignored.add(name) + continue + # 4. Belt-and-suspenders: check if file is in an excluded dir (rel_item prefix match) + if any(rel_item.startswith(p) for p in MD5_EXCLUDE_PREFIXES): + ignored.add(name) + continue + + return ignored + + # symlinks=True preserves symlinks in the copy (though we ignore them above). + # Re-using shutil.copytree is more robust than a manual walk for edge cases. + # Note: we already ignored symlinks in our ignore_logic to match hash behavior. + shutil.copytree(source_root, target_dir, symlinks=True, ignore=ignore_logic, dirs_exist_ok=True) + + +def _write_hash_file(image_dir: Path, payload: dict, log) -> None: + """Write .code-hash.json with fixed field order (D-07).""" + hash_path = image_dir / _HASH_FILENAME + # Ensure field order per specifics §1 + ordered = { + "hash": payload["hash"], + "algorithm": payload["algorithm"], + "captured_at": payload["captured_at"], + "mlpstorage_version": payload["mlpstorage_version"], + "git_sha": payload["git_sha"], + } + with open(hash_path, "w", encoding="utf-8") as f: + json.dump(ordered, f, indent=2) + f.write("\n") + + +def _read_hash_file(image_dir: Path, log) -> dict: + """Read and validate the JSON file (D-15).""" + hash_path = image_dir / _HASH_FILENAME + if not hash_path.is_file(): + raise MissingHashFile(f"Required file {_HASH_FILENAME} not found at {hash_path}") + + try: + with open(hash_path, "r", encoding="utf-8") as f: + data = json.load(f) + except (json.JSONDecodeError, UnicodeDecodeError) as e: + raise MalformedHashFile(f"Failed to parse {_HASH_FILENAME} at {hash_path}: {e}") + + # Validation + required = ["hash", "algorithm", "captured_at", "mlpstorage_version", "git_sha"] + for field in required: + if field not in data: + raise MalformedHashFile(f"Missing required field '{field}' in {hash_path}") + + if data["algorithm"] != _ALGORITHM: + raise MalformedHashFile(f"Unknown algorithm '{data['algorithm']}' (expected '{_ALGORITHM}') in {hash_path}") + + if not re.fullmatch(r"[0-9a-f]{" + str(_HASH_HEX_LEN) + r"}", data["hash"]): + raise MalformedHashFile(f"Invalid MD5 hash format in {hash_path}") + + if data["git_sha"] is not None: + if not re.fullmatch(r"[0-9a-f]{" + str(_GIT_SHA_LEN) + r"}", data["git_sha"]): + raise MalformedHashFile(f"Invalid git_sha format in {hash_path}") + + if not re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", data["captured_at"]): + raise MalformedHashFile(f"Invalid captured_at timestamp format in {hash_path}") + + return data + + +def _resolve_git_sha(source_root: Path, log) -> str | None: + """Best-effort capture of HEAD SHA (D-08).""" + try: + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=str(source_root), + capture_output=True, + text=True, + check=False, + timeout=_GIT_TIMEOUT_SEC, + shell=False, + ) + if result.returncode == 0: + sha = result.stdout.strip() + if re.fullmatch(r"[0-9a-f]{" + str(_GIT_SHA_LEN) + r"}", sha): + return sha + except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired) as e: + log.warning("Failed to resolve git SHA in %s: %s (D-08)", source_root, e) + + return None + + +def _now_utc_iso() -> str: + """Return canonical ISO-8601 UTC 'Z' timestamp (D-10).""" + return datetime.datetime.now(tz=datetime.UTC).isoformat(timespec="seconds").replace("+00:00", "Z") + + +# --------------------------------------------------------------------------- +# CLI dispatch helper (Phase 2 — D-07..D-10, D-20, D-21) +# --------------------------------------------------------------------------- + +def capture_or_verify_code_image(args, env, log): + """Capture-or-verify the code image at the submission tree (D-07..D-10). + + The single CLI dispatch chokepoint that owns the entire CAP/VALR contract: + + - Gates on `(args.mode, args.command)`: returns None unless mode is in + {closed, open} AND command is in {datasize, datagen, run} (D-10). + - Reads + validates MLPSTORAGE_ORGNAME (and MLPSTORAGE_SYSTEMNAME for OPEN) + from `env` — this helper is the SOLE reader of those env vars in the + codebase (Gemini MEDIUM trust-contract finding closed; D-05). + - Applies POSIX regex (Rules.md §2.1.1) AND inline `.`/`..` path-traversal + guard for both orgname and systemname (T-02-02-05 mitigation, REVIEWS.md + consensus finding). + - Computes the image-parent path matching `generate_output_location`'s + prefix (Plan 01, D-03). Stores validated values on `args` so downstream + `generate_output_location` callers can read them without re-reading env. + - Captures (CAP-01/02/06) on first call, verifies (VALR-01/03 success, + VALR-02/04 mismatch) on subsequent calls. Re-raises Phase 1 typed errors + (MissingHashFile, MalformedHashFile) after logging the D-21 recovery + message; mismatch raises CodeImageError with the literal spec string. + + Args: + args: argparse.Namespace-like with attributes `mode`, `command`, + `results_dir`, `benchmark`, `model`. + env: Mapping (e.g., os.environ) used to look up MLPSTORAGE_* env vars. + log: Logger object with status/error/info/warning/debug methods. + + Returns: + Path | None: The captured/verified `code/` directory path, or None + when gated off. + + Raises: + ConfigurationError: Missing or invalid MLPSTORAGE_* env var. + CodeImageError: Hash mismatch (VALR-02/04) — main() maps to + EXIT_CODE.CODE_IMAGE_ERROR. + MissingHashFile / MalformedHashFile: Existing code/ has missing or + unparseable .code-hash.json (D-21) — main() maps to exit code 2. + SourceRootNotFound: Live source tree could not be located/hashed. + + Notes: + D-07..D-10, D-20, D-21; inline path-traversal guard per REVIEWS.md + consensus finding (T-02-02-05). This helper is the SOLE reader of + MLPSTORAGE_ORGNAME / MLPSTORAGE_SYSTEMNAME env vars. + """ + # 1. Gate by mode (D-10) — return None for whatif/reports/validate/etc. + mode = getattr(args, "mode", None) + if mode not in _SUBMISSION_MODES: + return None + + # 2. Gate by command (D-10) — return None for configview/etc. under + # closed|open modes (e.g., `mlpstorage closed configview`). + command = getattr(args, "command", None) + if command not in _SUBMISSION_COMMANDS: + return None + + # 3. Read + validate orgname (D-04, D-05). + orgname = env.get(MLPSTORAGE_ORGNAME_ENVVAR) + if not orgname: + raise ConfigurationError( + "MLPSTORAGE_ORGNAME environment variable is required for closed|open runs", + parameter=MLPSTORAGE_ORGNAME_ENVVAR, + suggestion=( + "export MLPSTORAGE_ORGNAME= " + "# future: mlpstorage init " + ), + code=ErrorCode.CONFIG_MISSING_REQUIRED, + ) + if not _SUBMITTER_NAME_RE.match(orgname): + raise ConfigurationError( + f"MLPSTORAGE_ORGNAME={orgname!r} is not a POSIX-filename-safe identifier " + f"(Rules.md §2.1.1: ^[A-Za-z0-9._-]+$)", + parameter=MLPSTORAGE_ORGNAME_ENVVAR, + suggestion="Use only letters, digits, '.', '_', or '-'", + code=ErrorCode.CONFIG_INVALID_VALUE, + ) + # INLINE path-traversal guard for orgname (CONSENSUS FINDING — REVIEWS.md). + # The regex `^[A-Za-z0-9._-]+$` accepts `.` and `..` literally, so an + # additional explicit reject is REQUIRED. The substring `"'.' and '..' + # are reserved path segments"` is the spec contract used by Plan 05's tests. + if orgname in _RESERVED_PATH_SEGMENTS: + raise ConfigurationError( + f"MLPSTORAGE_ORGNAME={orgname!r} is not a permitted value: " + f"'.' and '..' are reserved path segments", + parameter=MLPSTORAGE_ORGNAME_ENVVAR, + suggestion="Choose an orgname that is not '.' or '..'", + code=ErrorCode.CONFIG_INVALID_VALUE, + ) + + # 4. For OPEN, also read + validate systemname. + systemname = None + if mode == "open": + systemname = env.get(MLPSTORAGE_SYSTEMNAME_ENVVAR) + if not systemname: + raise ConfigurationError( + "MLPSTORAGE_SYSTEMNAME environment variable is required for open runs", + parameter=MLPSTORAGE_SYSTEMNAME_ENVVAR, + suggestion=( + "export MLPSTORAGE_SYSTEMNAME= " + "# future: per-command --system-name flag" + ), + code=ErrorCode.CONFIG_MISSING_REQUIRED, + ) + if not _SUBMITTER_NAME_RE.match(systemname): + raise ConfigurationError( + f"MLPSTORAGE_SYSTEMNAME={systemname!r} is not a POSIX-filename-safe identifier " + f"(Rules.md §2.1.1: ^[A-Za-z0-9._-]+$)", + parameter=MLPSTORAGE_SYSTEMNAME_ENVVAR, + suggestion="Use only letters, digits, '.', '_', or '-'", + code=ErrorCode.CONFIG_INVALID_VALUE, + ) + # INLINE path-traversal guard for systemname (CONSENSUS FINDING — REVIEWS.md). + if systemname in _RESERVED_PATH_SEGMENTS: + raise ConfigurationError( + f"MLPSTORAGE_SYSTEMNAME={systemname!r} is not a permitted value: " + f"'.' and '..' are reserved path segments", + parameter=MLPSTORAGE_SYSTEMNAME_ENVVAR, + suggestion="Choose a systemname that is not '.' or '..'", + code=ErrorCode.CONFIG_INVALID_VALUE, + ) + + # 5. Stash validated values on args so downstream generate_output_location + # callers can consume them without re-reading env (closes the Gemini MEDIUM + # trust-contract finding — this helper remains the sole env reader). + args._validated_orgname = orgname + args._validated_systemname = systemname + + # 6. Compute image_parent — MUST match Plan 01's generate_output_location + # prefix. The helper only creates the {closed|open}//.../code/ + # subtree inside the already-existing results-directory (D-06); creating + # the results-directory itself is reserved for the future + # `mlpstorage init` command. + results_dir = Path(args.results_dir) + # IN-03: enforce the "results_dir must already exist" contract from the + # comment above. Without this gate, image_parent.mkdir(parents=True, ...) + # below silently creates results_dir if absent, diverging from the + # documented behavior. + if not results_dir.exists(): + raise ConfigurationError( + f"results_dir {str(results_dir)!r} does not exist; the code-image " + f"helper does not create it (reserved for future `mlpstorage init`)", + parameter="--results-dir", + suggestion=f"mkdir -p {str(results_dir)!r} before running, " + f"or point --results-dir at an existing directory", + code=ErrorCode.CONFIG_INVALID_VALUE, + ) + if mode == "closed": + image_parent = results_dir / "closed" / orgname + else: # mode == "open" + # Canonicalize the per-type segment via _CLI_BENCHMARK_TO_TYPE + + # _TYPE_TO_ONDISK_SEGMENT so the captured code/ shares the on-disk + # tree with generate_output_location's output. The CLI subparser + # names 'vectordb' and 'kvcache' diverge from the on-disk segments + # ('vector_database' and 'kv_cache') — without these lookups the + # captured code/ would live in a different tree than the runtime's + # results. + # Use getattr(..., None) + typed raise rather than bare getattr. + # A bare getattr surfaces AttributeError, which the main.py exit-code + # mapping treats as an unhandled crash rather than CodeImageError. + cli_benchmark = getattr(args, "benchmark", None) + if cli_benchmark is None: + raise CodeImageError( + "args.benchmark is required for capture-or-verify in OPEN mode" + ) + try: + benchmark_type = _CLI_BENCHMARK_TO_TYPE[cli_benchmark] + except KeyError: + raise CodeImageError( + f"Unknown benchmark CLI name {cli_benchmark!r} — " + f"expected one of {sorted(_CLI_BENCHMARK_TO_TYPE)}" + ) from None + ondisk_segment = _TYPE_TO_ONDISK_SEGMENT[benchmark_type] + leaf_dir = ( + results_dir / "open" / orgname / "results" / systemname + / ondisk_segment + ) + # Per-type leaf segment (see _TYPE_TO_LEAF_ATTR for the design rationale). + leaf_attr = _TYPE_TO_LEAF_ATTR[benchmark_type] + if leaf_attr is not None: + leaf_value = getattr(args, leaf_attr, None) + if leaf_value is None: + raise CodeImageError( + f"args.{leaf_attr} is required for " + f"{benchmark_type.name} OPEN capture" + ) + leaf_dir = leaf_dir / leaf_value + image_parent = leaf_dir + image_parent.mkdir(parents=True, exist_ok=True) + + # 7. Branch capture-vs-verify (D-08). + code_dir = image_parent / _CODE_DIRNAME + source_root = find_source_root() + + if not code_dir.exists(): + capture_code_image(source_root, image_parent, log) + log.status(f"Captured code image at {code_dir}") + return code_dir + + # code_dir exists → verify path. Catch missing/malformed .code-hash.json + # so we can attach the D-21 actionable recovery message before re-raising. + try: + matched = verify_source_against_image(source_root, code_dir, log) + except (MissingHashFile, MalformedHashFile) as e: + log.error(str(e)) + log.error(f"code image at: {code_dir}") + log.error( + "either delete `code/` and re-run to re-capture, " + "or restore the original capture." + ) + raise + + if matched: + log.status(f"code unchanged from on-file image at {code_dir}") + return code_dir + + # Hash mismatch — emit the literal spec string by mode (VALR-02 / VALR-04). + if mode == "closed": + msg = "changes to the codebase are not allowed in a CLOSED run" + else: # mode == "open" + msg = "all runs of this type must use the same codebase" + log.error(msg) + log.error(f"code image at: {code_dir}") + raise CodeImageError(msg) diff --git a/mlpstorage_py/submission_checker/tools/rules_coverage.py b/mlpstorage_py/submission_checker/tools/rules_coverage.py index 26eb495b..92c3ac24 100644 --- a/mlpstorage_py/submission_checker/tools/rules_coverage.py +++ b/mlpstorage_py/submission_checker/tools/rules_coverage.py @@ -1,10 +1,10 @@ -"""Standalone CLI tool that reconciles Rules.md §2/§3/§4 against checker code. +"""Standalone CLI tool that reconciles Rules.md §2/§3/§4/§5/§6 against checker code. Usage: python -m mlpstorage_py.submission_checker.tools.rules_coverage [--rules-md PATH] -Walks every kebab-case rule ID in ``Rules.md`` §2/§3/§4 (matched by the locked -regex ``^([234]\\.\\d+\\.\\d+)\\.\\s+\\*\\*([a-zA-Z][a-zA-Z0-9]+)\\*\\*``) and +Walks every kebab-case rule ID in ``Rules.md`` §2/§3/§4/§5/§6 (matched by the locked +regex ``^([23456]\\.\\d+\\.\\d+)\\.\\s+\\*\\*([a-zA-Z][a-zA-Z0-9]+)\\*\\*``) and reconciles each ID against four coverage sources in priority order (D-A4): 1. ``@rule``-decorated method on any ``BaseCheck`` subclass (via @@ -46,8 +46,12 @@ log = logging.getLogger("rules_coverage") -# D-A3: locked regex for Rules.md §2/§3/§4 ID enumeration. -_RULE_ID_PATTERN = re.compile(r"^([234]\.\d+\.\d+)\.\s+\*\*([a-zA-Z][a-zA-Z0-9]+)\*\*") +# D-A3: locked regex for Rules.md §2/§3/§4/§5/§6 ID enumeration. +# §5 covers vectordb (VdbCheck, Phase 4); §6 reserved for kvcache rules +# landing soon. Extending the character class preemptively avoids +# repeating Phase 4's miss where the regex shipped behind the actual +# Rules.md scope. +_RULE_ID_PATTERN = re.compile(r"^([23456]\.\d+\.\d+)\.\s+\*\*([a-zA-Z][a-zA-Z0-9]+)\*\*") def _default_rules_md_path() -> str: @@ -245,7 +249,7 @@ def _compute_drift(live_ids: set) -> tuple: def reconcile(rules_md_path=None) -> dict: - """Reconcile Rules.md §2/§3/§4 IDs against the four coverage sources. + """Reconcile Rules.md §2/§3/§4/§5/§6 IDs against the four coverage sources. Applies the locked priority order from CONTEXT.md D-A4: @@ -400,7 +404,7 @@ def get_args(): """ parser = argparse.ArgumentParser( description=( - "Reconcile every Rules.md §2/§3/§4 ID against @rule-decorated " + "Reconcile every Rules.md §2/§3/§4/§5/§6 ID against @rule-decorated " "check methods, SCHEMA_ERROR_RULE_MAP, STUB_COVERAGE, and " "OUT_OF_SCOPE_RULES. Exits 1 if any ID is unmapped." ), diff --git a/mlpstorage_py/tests/test_capture_or_verify_code_image.py b/mlpstorage_py/tests/test_capture_or_verify_code_image.py new file mode 100644 index 00000000..dd4f03a6 --- /dev/null +++ b/mlpstorage_py/tests/test_capture_or_verify_code_image.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +""" +Tests for mlpstorage_py.submission_checker.tools.code_image.capture_or_verify_code_image. + +Covers Phase 2 D-07..D-10, D-20, D-21 and the consensus INLINE `.`/`..` +path-traversal guard (T-02-02-05 mitigation made inline). + +Run with: + pytest mlpstorage_py/tests/test_capture_or_verify_code_image.py -v +""" + +import json +import re +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from mlpstorage_py.errors import ConfigurationError, ErrorCode +from mlpstorage_py.submission_checker.tools.code_image import ( + CodeImageError, + MissingHashFile, + capture_or_verify_code_image, + _SUBMITTER_NAME_RE, + _RESERVED_PATH_SEGMENTS, +) + + +# --------------------------------------------------------------------------- +# MockLogger that captures status/error calls for assertion. +# --------------------------------------------------------------------------- + +class MockLogger: + def __init__(self): + self.statuses = [] + self.errors = [] + self.warnings = [] + self.infos = [] + self.debugs = [] + + def status(self, msg, *args): + self.statuses.append(msg % args if args else msg) + + def error(self, msg, *args): + self.errors.append(msg % args if args else msg) + + def warning(self, msg, *args): + self.warnings.append(msg % args if args else msg) + + def info(self, msg, *args): + self.infos.append(msg % args if args else msg) + + def debug(self, msg, *args): + self.debugs.append(msg % args if args else msg) + + # Phase 1 verbose levels (unused here but kept for compatibility) + def verbose(self, msg, *args): pass + def verboser(self, msg, *args): pass + def ridiculous(self, msg, *args): pass + + +@pytest.fixture +def log(): + return MockLogger() + + +def _make_args(*, mode, command, results_dir, benchmark="training", model="unet3d"): + return SimpleNamespace( + mode=mode, + command=command, + results_dir=str(results_dir), + benchmark=benchmark, + model=model, + ) + + +# --------------------------------------------------------------------------- +# Module-level constant sanity +# --------------------------------------------------------------------------- + +class TestModuleConstants: + def test_submitter_name_regex_compiled(self): + assert _SUBMITTER_NAME_RE.match("acme_corp.v1-2") is not None + assert _SUBMITTER_NAME_RE.match("bad name") is None + assert _SUBMITTER_NAME_RE.match("path/with/slash") is None + + def test_reserved_path_segments(self): + assert _RESERVED_PATH_SEGMENTS == frozenset({".", ".."}) + + def test_regex_accepts_dot_and_dotdot(self): + # The regex `^[A-Za-z0-9._-]+$` literally matches `.` and `..` — + # this is exactly why the additional reserved-segments guard is needed. + assert _SUBMITTER_NAME_RE.match(".") is not None + assert _SUBMITTER_NAME_RE.match("..") is not None + + +# --------------------------------------------------------------------------- +# Gating contract (D-10) — no env reads, no fs ops for non-submission modes +# --------------------------------------------------------------------------- + +class TestGatingContract: + def test_whatif_returns_none(self, tmp_path, log): + args = _make_args(mode="whatif", command="run", results_dir=tmp_path) + assert capture_or_verify_code_image(args, {}, log) is None + assert log.statuses == [] + assert log.errors == [] + + @pytest.mark.parametrize("mode", [ + "reports", "validate", "history", "lockfile", "version", "rules-coverage", + ]) + def test_non_submission_modes_return_none(self, tmp_path, log, mode): + args = _make_args(mode=mode, command="run", results_dir=tmp_path) + assert capture_or_verify_code_image(args, {}, log) is None + + @pytest.mark.parametrize("command", [ + "configview", "validate", "datasize-something-else", + ]) + def test_non_submission_commands_return_none(self, tmp_path, log, command): + # mode is closed but command is not in {datasize, datagen, run} → no-op + args = _make_args(mode="closed", command=command, results_dir=tmp_path) + assert capture_or_verify_code_image(args, {}, log) is None + + +# --------------------------------------------------------------------------- +# Env-var fail-fast (D-04, D-05) +# --------------------------------------------------------------------------- + +class TestEnvVarFailFast: + def test_missing_orgname_raises_configuration_error(self, tmp_path, log): + args = _make_args(mode="closed", command="datagen", results_dir=tmp_path) + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, {}, log) + assert "MLPSTORAGE_ORGNAME" in str(exc_info.value) + assert exc_info.value.parameter == "MLPSTORAGE_ORGNAME" + assert "mlpstorage init" in (exc_info.value.suggestion or "") + + def test_missing_systemname_raises_configuration_error(self, tmp_path, log): + args = _make_args(mode="open", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, log) + assert "MLPSTORAGE_SYSTEMNAME" in str(exc_info.value) + assert exc_info.value.parameter == "MLPSTORAGE_SYSTEMNAME" + + def test_orgname_with_space_rejected(self, tmp_path, log): + args = _make_args(mode="closed", command="run", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "bad name"} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, log) + assert "Rules.md" in str(exc_info.value) + + def test_orgname_with_slash_rejected(self, tmp_path, log): + args = _make_args(mode="closed", command="run", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "evil/path"} + with pytest.raises(ConfigurationError): + capture_or_verify_code_image(args, env, log) + + +# --------------------------------------------------------------------------- +# INLINE path-traversal guard (CONSENSUS FINDING — T-02-02-05) +# --------------------------------------------------------------------------- + +class TestPathTraversalGuard: + def test_orgname_dot_rejected(self, tmp_path, log): + args = _make_args(mode="closed", command="run", results_dir=tmp_path) + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, {"MLPSTORAGE_ORGNAME": "."}, log) + msg = str(exc_info.value) + assert "'.' and '..' are reserved path segments" in msg + + def test_orgname_dotdot_rejected(self, tmp_path, log): + args = _make_args(mode="closed", command="run", results_dir=tmp_path) + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, {"MLPSTORAGE_ORGNAME": ".."}, log) + assert "'.' and '..' are reserved path segments" in str(exc_info.value) + + def test_systemname_dot_rejected(self, tmp_path, log): + args = _make_args(mode="open", command="run", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "."} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, log) + assert "'.' and '..' are reserved path segments" in str(exc_info.value) + + def test_systemname_dotdot_rejected(self, tmp_path, log): + args = _make_args(mode="open", command="run", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": ".."} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, log) + assert "'.' and '..' are reserved path segments" in str(exc_info.value) + + +# --------------------------------------------------------------------------- +# Capture path (CAP-01, CAP-02, CAP-06) +# --------------------------------------------------------------------------- + +class TestCapturePath: + def test_closed_first_run_captures(self, tmp_path, log): + args = _make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + result = capture_or_verify_code_image(args, env, log) + # CAP-02: CLOSED tree shape + expected_code = tmp_path / "closed" / "acme" / "code" + assert result == expected_code + assert expected_code.is_dir() + assert (expected_code / ".code-hash.json").is_file() + # CAP-06: log message starts "Captured code image at " + assert any(s.startswith(f"Captured code image at {expected_code}") for s in log.statuses), log.statuses + + def test_open_first_run_captures_per_leaf(self, tmp_path, log): + args = _make_args( + mode="open", command="run", results_dir=tmp_path, + benchmark="training", model="unet3d", + ) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "rig01"} + result = capture_or_verify_code_image(args, env, log) + expected_code = ( + tmp_path / "open" / "acme" / "results" / "rig01" / "training" / "unet3d" / "code" + ) + assert result == expected_code + assert expected_code.is_dir() + + def test_open_vectordb_uses_canonical_type_name(self, tmp_path, log): + """The CLI subparser is named 'vectordb', but the on-disk type segment + is 'vector_database' (BENCHMARK_TYPES.name). The helper must emit that + canonical on-disk segment so the captured code/ lives in the same + submission tree the runtime writes results into. + + vector_database splits results by because AISAQ results + are not comparable to DISKANN/HNSW. The captured code/ lives at + vector_database//code/ — per-leaf, same depth as + training/checkpointing. The index directory is the UPPERCASE token, + matching args.index_type and summary.json.index_type. + """ + args = SimpleNamespace( + mode="open", command="run", results_dir=str(tmp_path), + benchmark="vectordb", index_type="DISKANN", + ) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "rig01"} + result = capture_or_verify_code_image(args, env, log) + expected_code = ( + tmp_path / "open" / "acme" / "results" / "rig01" + / "vector_database" / "DISKANN" / "code" + ) + assert result == expected_code + # And the CLI name 'vectordb' must NOT appear as a path segment. + assert "vectordb" not in {p.name for p in result.parents} + + def test_open_kvcache_uses_canonical_type_name(self, tmp_path, log): + """Same contract as vectordb: CLI name 'kvcache' must map to canonical + on-disk segment 'kv_cache' (BENCHMARK_TYPES.name). + + Like vector_database, kv_cache writes /// — + no in the runtime path — so the captured code/ also lives + directly under /. + """ + # kvcache does have --model in CLI, but the helper must ignore it + # because the runtime path-shape has no model segment. + args = _make_args( + mode="open", command="run", results_dir=tmp_path, + benchmark="kvcache", model="llama3-8b", + ) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "rig01"} + result = capture_or_verify_code_image(args, env, log) + expected_code = ( + tmp_path / "open" / "acme" / "results" / "rig01" + / "kv_cache" / "code" + ) + assert result == expected_code + assert "kvcache" not in {p.name for p in result.parents} + # model segment must not appear in the captured path. + assert "llama3-8b" not in {p.name for p in result.parents} + + +# --------------------------------------------------------------------------- +# Verify path (VALR-01/03 success; VALR-02/04 mismatch; D-21 missing-json) +# --------------------------------------------------------------------------- + +class TestVerifyPath: + def test_matching_code_image_verifies_silently(self, tmp_path, log, monkeypatch): + # Use an isolated source tree to keep the live-source hash deterministic + # (the real repo's untracked / non-copytree-able files would otherwise + # diverge between capture-via-shutil and live-source hashing). + src = tmp_path / "iso_src" + src.mkdir() + (src / "a.py").write_bytes(b"A\n") + (src / "pyproject.toml").write_bytes(b"# stub\n") + + import mlpstorage_py.submission_checker.tools.code_image as mod + monkeypatch.setattr(mod, "find_source_root", lambda: src) + + # First call captures. + args = _make_args(mode="closed", command="datasize", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + code_dir = capture_or_verify_code_image(args, env, log) + log.statuses.clear() + # Second call should verify and pass. + result = capture_or_verify_code_image(args, env, log) + assert result == code_dir + assert any( + f"code unchanged from on-file image at {code_dir}" in s for s in log.statuses + ), log.statuses + + def test_closed_mismatch_raises_codeimage_error_with_literal(self, tmp_path, log, monkeypatch): + args = _make_args(mode="closed", command="run", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + capture_or_verify_code_image(args, env, log) + + # Force a hash mismatch by monkeypatching verify_source_against_image to return False. + import mlpstorage_py.submission_checker.tools.code_image as mod + monkeypatch.setattr(mod, "verify_source_against_image", lambda *a, **k: False) + + log.errors.clear() + with pytest.raises(CodeImageError) as exc_info: + capture_or_verify_code_image(args, env, log) + assert "changes to the codebase are not allowed in a CLOSED run" in str(exc_info.value) + assert any( + "changes to the codebase are not allowed in a CLOSED run" in e for e in log.errors + ), log.errors + + def test_open_mismatch_raises_codeimage_error_with_literal(self, tmp_path, log, monkeypatch): + args = _make_args( + mode="open", command="run", results_dir=tmp_path, + benchmark="training", model="unet3d", + ) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "rig01"} + capture_or_verify_code_image(args, env, log) + + import mlpstorage_py.submission_checker.tools.code_image as mod + monkeypatch.setattr(mod, "verify_source_against_image", lambda *a, **k: False) + + log.errors.clear() + with pytest.raises(CodeImageError) as exc_info: + capture_or_verify_code_image(args, env, log) + assert "all runs of this type must use the same codebase" in str(exc_info.value) + + def test_missing_hash_file_logs_recovery_and_reraises(self, tmp_path, log): + # Pre-create a code/ directory without .code-hash.json + code_dir = tmp_path / "closed" / "acme" / "code" + code_dir.mkdir(parents=True) + (code_dir / "dummy.py").write_text("# placeholder") + + args = _make_args(mode="closed", command="run", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + with pytest.raises(MissingHashFile): + capture_or_verify_code_image(args, env, log) + # D-21 actionable recovery substring + assert any( + "either delete `code/` and re-run to re-capture" in e for e in log.errors + ), log.errors diff --git a/mlpstorage_py/tests/test_cli_code_image.py b/mlpstorage_py/tests/test_cli_code_image.py new file mode 100644 index 00000000..af12a336 --- /dev/null +++ b/mlpstorage_py/tests/test_cli_code_image.py @@ -0,0 +1,530 @@ +#!/usr/bin/env python3 +"""Phase 2 Plan 02-05 — CAP/VALR contract tests for the CLI dispatch helper. + +Covers requirements: + CAP-01, CAP-02, CAP-06, CAP-07, CAP-08 + VALR-01, VALR-02, VALR-03, VALR-04 + D-04, D-05, D-21 + Path-traversal '.' / '..' rejection (REVIEWS.md consensus finding, + Gemini + plan-checker — _RESERVED_PATH_SEGMENTS guard). + +Tests exercise ``capture_or_verify_code_image(args, env, log)`` via direct +in-process invocation with ``tmp_path`` + MockLogger fixtures (CD-02 — +chosen lightweight style, no subprocess / no MPI). + +Run with: + pytest mlpstorage_py/tests/test_cli_code_image.py -v +""" + +import json +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from mlpstorage_py.submission_checker.tools.code_image import ( + capture_or_verify_code_image, + capture_code_image, + CodeImageError, + MissingHashFile, + MalformedHashFile, +) +from mlpstorage_py.errors import ConfigurationError + + +# --------------------------------------------------------------------------- +# MockLogger — captures status/warning/error/info/debug calls. +# Mirrors the PATTERNS.md "Imports + MockLogger pattern" with the extra +# ``status`` channel that the Phase 2 helper uses for CAP-06 / VALR-01/03 +# success messages. +# --------------------------------------------------------------------------- + +class MockLogger: + def __init__(self): + self.warnings = [] + self.errors = [] + self.infos = [] + self.debugs = [] + self.statuses = [] + + def debug(self, msg, *a): self.debugs.append(msg % a if a else msg) + def info(self, msg, *a): self.infos.append(msg % a if a else msg) + def status(self, msg, *a): self.statuses.append(msg % a if a else msg) + def warning(self, msg, *a): self.warnings.append(msg % a if a else msg) + def error(self, msg, *a): self.errors.append(msg % a if a else msg) + def verbose(self, *a, **k): pass + def verboser(self, *a, **k): pass + def ridiculous(self, *a, **k): pass + + +@pytest.fixture +def mock_logger(): + return MockLogger() + + +# --------------------------------------------------------------------------- +# fake_source_root — isolated tmp source tree to keep the live-source hash +# deterministic across capture (shutil.copytree+ignore) and verify +# (compute_code_tree_md5 direct walk). Documented in deferred-items.md as a +# Phase 1 follow-up; the workaround is the same pattern Plan 02-02's tests use. +# --------------------------------------------------------------------------- + +@pytest.fixture +def fake_source_root(tmp_path, monkeypatch): + src = tmp_path / "src_root" + src.mkdir() + (src / "pyproject.toml").write_text("[project]\nname = 'x'\nversion='0.0.1'\n") + (src / "mlpstorage_py").mkdir() + (src / "mlpstorage_py" / "__init__.py").write_text("__version__ = '0.0.1'\n") + (src / "mlpstorage_py" / "stub.py").write_text("X = 1\n") + monkeypatch.setattr( + "mlpstorage_py.submission_checker.tools.code_image.find_source_root", + lambda: src, + ) + return src + + +# --------------------------------------------------------------------------- +# make_args helper — small factory matching the helper's args shape. +# --------------------------------------------------------------------------- + +def make_args(*, mode, command, results_dir, benchmark="training", model="unet3d"): + return SimpleNamespace( + mode=mode, + command=command, + results_dir=str(results_dir), + benchmark=benchmark, + model=model, + ) + + +# --------------------------------------------------------------------------- +# TestClosedFirstCapture (CAP-01, CAP-06, TEST-02) +# --------------------------------------------------------------------------- + +class TestClosedFirstCapture: + """CAP-01: first call on closed|datagen captures the image at + {results_dir}/closed//code/. + """ + + def test_closed_first_capture_creates_code_dir( + self, tmp_path, fake_source_root, mock_logger + ): + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + result = capture_or_verify_code_image(args, env, mock_logger) + expected = tmp_path / "closed" / "acme" / "code" + assert result == expected + assert expected.is_dir() + assert (expected / ".code-hash.json").is_file() + + def test_closed_first_capture_logs_absolute_path( + self, tmp_path, fake_source_root, mock_logger + ): + # CAP-06: log starts with "Captured code image at " followed by the + # absolute code/ path. + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + capture_or_verify_code_image(args, env, mock_logger) + expected = tmp_path / "closed" / "acme" / "code" + assert any( + s.startswith("Captured code image at ") and str(expected) in s + for s in mock_logger.statuses + ), mock_logger.statuses + + +# --------------------------------------------------------------------------- +# TestOpenFirstCapture (CAP-02, CAP-06, TEST-03) +# --------------------------------------------------------------------------- + +class TestOpenFirstCapture: + """CAP-02: first call on open|datagen captures the image at + {results_dir}/open//results////code/. + """ + + def test_open_first_capture_creates_per_leaf_code_dir( + self, tmp_path, fake_source_root, mock_logger + ): + args = make_args( + mode="open", command="datagen", results_dir=tmp_path, + benchmark="training", model="unet3d", + ) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "sys-1"} + result = capture_or_verify_code_image(args, env, mock_logger) + expected = ( + tmp_path / "open" / "acme" / "results" / "sys-1" + / "training" / "unet3d" / "code" + ) + assert result == expected + assert expected.is_dir() + assert (expected / ".code-hash.json").is_file() + + def test_open_first_capture_logs_absolute_path( + self, tmp_path, fake_source_root, mock_logger + ): + args = make_args( + mode="open", command="datagen", results_dir=tmp_path, + benchmark="training", model="unet3d", + ) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "sys-1"} + capture_or_verify_code_image(args, env, mock_logger) + expected = ( + tmp_path / "open" / "acme" / "results" / "sys-1" + / "training" / "unet3d" / "code" + ) + assert any( + s.startswith("Captured code image at ") and str(expected) in s + for s in mock_logger.statuses + ), mock_logger.statuses + + +# --------------------------------------------------------------------------- +# TestRuntimeMatchPasses (VALR-01, VALR-03, TEST-04) +# --------------------------------------------------------------------------- + +class TestRuntimeMatchPasses: + """VALR-01/03: second call against an unchanged tree logs the + 'code unchanged from on-file image at ' status and returns the path. + """ + + def test_closed_second_run_matches( + self, tmp_path, fake_source_root, mock_logger + ): + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + # First call captures + first = capture_or_verify_code_image(args, env, mock_logger) + mock_logger.statuses.clear() + # Second call must verify silently + second = capture_or_verify_code_image(args, env, mock_logger) + assert second == first + expected = tmp_path / "closed" / "acme" / "code" + assert any( + f"code unchanged from on-file image at {expected}" in s + for s in mock_logger.statuses + ), mock_logger.statuses + + def test_open_second_run_matches( + self, tmp_path, fake_source_root, mock_logger + ): + args = make_args( + mode="open", command="datagen", results_dir=tmp_path, + benchmark="training", model="unet3d", + ) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "sys-1"} + first = capture_or_verify_code_image(args, env, mock_logger) + mock_logger.statuses.clear() + second = capture_or_verify_code_image(args, env, mock_logger) + assert second == first + expected = ( + tmp_path / "open" / "acme" / "results" / "sys-1" + / "training" / "unet3d" / "code" + ) + assert any( + f"code unchanged from on-file image at {expected}" in s + for s in mock_logger.statuses + ), mock_logger.statuses + + +# --------------------------------------------------------------------------- +# TestRuntimeMismatchCLOSED (VALR-02, TEST-05) +# --------------------------------------------------------------------------- + +class TestRuntimeMismatchCLOSED: + """VALR-02: on hash mismatch in a CLOSED run, raise CodeImageError + containing the literal spec string + 'changes to the codebase are not allowed in a CLOSED run'. + """ + + def test_closed_mismatch_raises_with_literal_message( + self, tmp_path, fake_source_root, mock_logger, monkeypatch + ): + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + # First call captures successfully. + capture_or_verify_code_image(args, env, mock_logger) + + # Force a hash mismatch on the second call by monkeypatching + # verify_source_against_image to return False. This isolates the + # mismatch code path from the Phase 1 capture-vs-verify hash + # discrepancy documented in deferred-items.md. + import mlpstorage_py.submission_checker.tools.code_image as mod + monkeypatch.setattr(mod, "verify_source_against_image", lambda *a, **k: False) + + mock_logger.errors.clear() + with pytest.raises(CodeImageError) as exc_info: + capture_or_verify_code_image(args, env, mock_logger) + # Literal spec string (VALR-02 substring match) — required by + # deep_work_rules. Assert against BOTH the raised exception and the + # logger so a future regression that drops one path still fails. + assert "changes to the codebase are not allowed in a CLOSED run" in str(exc_info.value) + assert any( + "changes to the codebase are not allowed in a CLOSED run" in e + for e in mock_logger.errors + ), mock_logger.errors + code_dir = tmp_path / "closed" / "acme" / "code" + assert any(f"code image at: {code_dir}" in e for e in mock_logger.errors), mock_logger.errors + + +# --------------------------------------------------------------------------- +# TestRuntimeMismatchOPEN (VALR-04, TEST-06) +# --------------------------------------------------------------------------- + +class TestRuntimeMismatchOPEN: + """VALR-04: on hash mismatch in an OPEN run, raise CodeImageError + containing the literal spec string + 'all runs of this type must use the same codebase'. + """ + + def test_open_mismatch_raises_with_literal_message( + self, tmp_path, fake_source_root, mock_logger, monkeypatch + ): + args = make_args( + mode="open", command="datagen", results_dir=tmp_path, + benchmark="training", model="unet3d", + ) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "sys-1"} + capture_or_verify_code_image(args, env, mock_logger) + + import mlpstorage_py.submission_checker.tools.code_image as mod + monkeypatch.setattr(mod, "verify_source_against_image", lambda *a, **k: False) + + mock_logger.errors.clear() + with pytest.raises(CodeImageError) as exc_info: + capture_or_verify_code_image(args, env, mock_logger) + assert "all runs of this type must use the same codebase" in str(exc_info.value) + assert any( + "all runs of this type must use the same codebase" in e + for e in mock_logger.errors + ), mock_logger.errors + code_dir = ( + tmp_path / "open" / "acme" / "results" / "sys-1" + / "training" / "unet3d" / "code" + ) + assert any(f"code image at: {code_dir}" in e for e in mock_logger.errors), mock_logger.errors + + +# --------------------------------------------------------------------------- +# TestNoTouchSubcommands (CAP-07, CAP-08, TEST-09) +# --------------------------------------------------------------------------- + +# Parametrized over the seven non-result-generating modes. The helper must +# return None and perform NO filesystem operations or env reads for each. +_NO_TOUCH_MODES = [ + "whatif", + "reports", + "validate", + "history", + "lockfile", + "version", + "rules-coverage", +] + + +class TestNoTouchSubcommands: + """CAP-07/08: helper is a no-op for whatif/validate/reportgen/etc. AND + for {closed|open} commands that are not in {datasize, datagen, run}. + """ + + @pytest.mark.parametrize("mode", _NO_TOUCH_MODES) + def test_no_touch(self, tmp_path, mock_logger, mode): + # An empty env confirms the helper does NOT read MLPSTORAGE_* env vars + # in the gated-off path (CAP-07/08). + args = make_args(mode=mode, command="run", results_dir=tmp_path) + env = {} + result = capture_or_verify_code_image(args, env, mock_logger) + assert result is None + # No subdirectories created under tmp_path. + assert not (tmp_path / "closed").exists() + assert not (tmp_path / "open").exists() + # No logger calls (gate runs before any logging in the helper). + assert mock_logger.statuses == [] + assert mock_logger.errors == [] + assert mock_logger.warnings == [] + assert mock_logger.infos == [] + + def test_no_touch_invalid_command_under_valid_mode(self, tmp_path, mock_logger): + # Under closed|open mode, command not in {datasize, datagen, run} → + # helper still returns None and performs no fs/env work. + args = make_args(mode="closed", command="configview", results_dir=tmp_path) + env = {} + result = capture_or_verify_code_image(args, env, mock_logger) + assert result is None + assert not (tmp_path / "closed").exists() + assert mock_logger.errors == [] + assert mock_logger.warnings == [] + + def test_no_touch_open_with_configview_command(self, tmp_path, mock_logger): + # Under open mode, command not in {datasize, datagen, run} → + # helper still returns None and performs no fs/env work. + args = make_args(mode="open", command="history", results_dir=tmp_path) + env = {} + result = capture_or_verify_code_image(args, env, mock_logger) + assert result is None + assert not (tmp_path / "open").exists() + assert mock_logger.errors == [] + assert mock_logger.warnings == [] + + @pytest.mark.parametrize("command", ["datasize", "datagen", "run"]) + def test_gating_passes_for_each_submission_command( + self, tmp_path, fake_source_root, mock_logger, command + ): + # Sanity: each of the three result-generating commands triggers + # capture-or-verify (returns a Path, creates code/), confirming the + # gating set membership and that no command in the spec is missed. + args = make_args(mode="closed", command=command, results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + result = capture_or_verify_code_image(args, env, mock_logger) + assert result is not None + assert result.is_dir() + + +# --------------------------------------------------------------------------- +# TestEnvVarValidation (D-04, D-05) +# --------------------------------------------------------------------------- + +class TestEnvVarValidation: + """Fail-fast on missing or POSIX-invalid MLPSTORAGE_* env vars.""" + + def test_missing_orgname_closed(self, tmp_path, mock_logger): + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, mock_logger) + msg = str(exc_info.value) + assert "MLPSTORAGE_ORGNAME" in msg + # ConfigurationError.suggestion should mention the future setup command. + suggestion = getattr(exc_info.value, "suggestion", "") or getattr( + exc_info.value.error, "suggestion", "" + ) + assert "mlpstorage init" in suggestion, suggestion + + def test_missing_systemname_open(self, tmp_path, mock_logger): + args = make_args(mode="open", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, mock_logger) + assert "MLPSTORAGE_SYSTEMNAME" in str(exc_info.value) + + def test_invalid_posix_orgname(self, tmp_path, mock_logger): + # Space is not in [A-Za-z0-9._-]. + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "bad name"} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, mock_logger) + assert "Rules.md §2.1.1" in str(exc_info.value) + assert "MLPSTORAGE_ORGNAME" in str(exc_info.value) + + def test_invalid_posix_systemname(self, tmp_path, mock_logger): + # Slash is not in [A-Za-z0-9._-] (path-traversal-adjacent). + args = make_args(mode="open", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": "with/slash"} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, mock_logger) + assert "Rules.md §2.1.1" in str(exc_info.value) + assert "MLPSTORAGE_SYSTEMNAME" in str(exc_info.value) + + +# --------------------------------------------------------------------------- +# TestEnvVarPathTraversal — CONSENSUS FINDING (Gemini + plan-checker) +# --------------------------------------------------------------------------- + +class TestEnvVarPathTraversal: + """REVIEWS.md consensus finding: the regex ^[A-Za-z0-9._-]+$ accepts '.' and + '..' literally. Plan 02 added an inline ``_RESERVED_PATH_SEGMENTS`` guard + AFTER the regex check. These tests pin that guard for BOTH env vars. + + Substring contract: the helper raises ConfigurationError with a message + containing the literal substring "'.' and '..' are reserved path segments". + """ + + @pytest.mark.parametrize("bad_value", [".", ".."]) + def test_orgname_dot_raises_configuration_error( + self, tmp_path, bad_value, mock_logger + ): + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": bad_value} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, mock_logger) + msg = str(exc_info.value) + assert "'.' and '..' are reserved path segments" in msg + assert "MLPSTORAGE_ORGNAME" in msg + + @pytest.mark.parametrize("bad_value", [".", ".."]) + def test_systemname_dot_raises_configuration_error( + self, tmp_path, bad_value, mock_logger + ): + args = make_args(mode="open", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme", "MLPSTORAGE_SYSTEMNAME": bad_value} + with pytest.raises(ConfigurationError) as exc_info: + capture_or_verify_code_image(args, env, mock_logger) + msg = str(exc_info.value) + assert "'.' and '..' are reserved path segments" in msg + assert "MLPSTORAGE_SYSTEMNAME" in msg + + def test_valid_names_pass_sanity_check( + self, tmp_path, fake_source_root, mock_logger + ): + """Sanity: valid POSIX names that are NOT '.'/'..' must NOT raise. + + Confirms that the rejection in the prior two tests is specifically + due to the '.'/'..' guard, not a different validation bug. + """ + args = make_args(mode="open", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "valid_name", "MLPSTORAGE_SYSTEMNAME": "valid_name"} + result = capture_or_verify_code_image(args, env, mock_logger) + assert result is not None + assert result.exists() + + def test_filesystem_unchanged_after_path_traversal_reject(self, tmp_path, mock_logger): + """The helper rejects BEFORE any mkdir — filesystem is untouched.""" + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "."} + with pytest.raises(ConfigurationError): + capture_or_verify_code_image(args, env, mock_logger) + assert not (tmp_path / "closed").exists() + assert not (tmp_path / "open").exists() + + +# --------------------------------------------------------------------------- +# TestBadImageRecovery (D-21) +# --------------------------------------------------------------------------- + +class TestBadImageRecovery: + """D-21: when an existing code/ has a missing or malformed .code-hash.json, + the helper logs the actionable recovery substring and re-raises the + Phase 1 typed error. + """ + + def test_missing_hash_file_logs_recovery_message(self, tmp_path, mock_logger): + # Pre-create code/ with files but NO .code-hash.json. + code_dir = tmp_path / "closed" / "acme" / "code" + code_dir.mkdir(parents=True) + (code_dir / "dummy.py").write_text("# placeholder\n") + + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + with pytest.raises(MissingHashFile): + capture_or_verify_code_image(args, env, mock_logger) + assert any( + "either delete `code/` and re-run to re-capture, or restore the original capture." + in e + for e in mock_logger.errors + ), mock_logger.errors + + def test_malformed_hash_file_logs_recovery_message(self, tmp_path, mock_logger): + # Pre-create code/ with an invalid .code-hash.json. + code_dir = tmp_path / "closed" / "acme" / "code" + code_dir.mkdir(parents=True) + (code_dir / "dummy.py").write_text("# placeholder\n") + (code_dir / ".code-hash.json").write_text("{not valid json") + + args = make_args(mode="closed", command="datagen", results_dir=tmp_path) + env = {"MLPSTORAGE_ORGNAME": "acme"} + with pytest.raises(MalformedHashFile): + capture_or_verify_code_image(args, env, mock_logger) + assert any( + "either delete `code/` and re-run to re-capture, or restore the original capture." + in e + for e in mock_logger.errors + ), mock_logger.errors diff --git a/mlpstorage_py/tests/test_code_checksum.py b/mlpstorage_py/tests/test_code_checksum.py index d617125c..5bf0faf8 100644 --- a/mlpstorage_py/tests/test_code_checksum.py +++ b/mlpstorage_py/tests/test_code_checksum.py @@ -241,6 +241,49 @@ def test_nonexistent_root_returns_none(self, tmp_path, mock_logger): assert result is None + def test_nested_excluded_dir_pruned_at_any_depth(self, tmp_path, mock_logger): + """Regression: directory names in MD5_EXCLUDE_PREFIXES must be pruned at any + depth, not only at the tree root. Without this, capture_code_image (which + excludes by basename via shutil.copytree ignore) diverges from + compute_code_tree_md5 (which only matched a rooted-prefix) and verify + spuriously fails for unchanged source on real repos.""" + from mlpstorage_py.submission_checker.tools.code_checksum import compute_code_tree_md5 + + # Tree A: real file plus a NON-pyc file inside a deeply nested __pycache__. + # Using a non-.pyc filename ensures the dir-level exclusion is what makes + # this pass — filename-level exclusion would not catch it. + tree_a = tmp_path / "tree_a" + write_binary(tree_a / "pkg" / "mod.py", b"x = 1\n") + write_binary(tree_a / "pkg" / "__pycache__" / "leak.txt", b"leaked\n") + write_binary(tree_a / "pkg" / "tests" / "leak.txt", b"also leaked\n") + write_binary(tree_a / "pkg" / "sub" / "build" / "artifact.bin", b"\x00") + + # Tree B: only the real file. + tree_b = tmp_path / "tree_b" + write_binary(tree_b / "pkg" / "mod.py", b"x = 1\n") + + assert compute_code_tree_md5(str(tree_a), mock_logger) == compute_code_tree_md5(str(tree_b), mock_logger) + + def test_capture_verify_roundtrip_with_nested_excluded_dirs(self, tmp_path, mock_logger): + """Regression for the cross-walker divergence: capture_code_image (basename + ignore) and verify_source_against_image (compute_code_tree_md5) must agree + on unchanged source even when the source has deeply nested __pycache__/ + directories with non-pyc files. Before the fix, the captured tree omitted + the deep dir but the source walker hashed it in, producing False.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + capture_code_image, verify_source_against_image, + ) + + source = tmp_path / "source" + write_binary(source / "pkg" / "main.py", b"print('hi')\n") + write_binary(source / "pkg" / "__pycache__" / "leak.txt", b"residue\n") + write_binary(source / "deep" / "tests" / "leak.txt", b"residue\n") + + image_parent = tmp_path / "out" + capture_code_image(source, image_parent, mock_logger) + + assert verify_source_against_image(source, image_parent / "code", mock_logger) is True + # --------------------------------------------------------------------------- # CLI integration tests (D-11) — added in Task 3 diff --git a/mlpstorage_py/tests/test_code_image.py b/mlpstorage_py/tests/test_code_image.py new file mode 100644 index 00000000..f4ff5b43 --- /dev/null +++ b/mlpstorage_py/tests/test_code_image.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python3 +""" +Tests for mlpstorage_py.submission_checker.tools.code_image.{capture,load,verify}. + +Covers D-01..D-20 capture/verify behaviors. + +Run with: + pytest mlpstorage_py/tests/test_code_image.py -v +""" + +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from mlpstorage_py import __version__ as MLPSTORAGE_VERSION + + +# --------------------------------------------------------------------------- +# MockLogger that captures warning() and error() calls for assertion. +# --------------------------------------------------------------------------- + +class MockLogger: + """Mock logger that captures warning/error messages for assertion.""" + + def __init__(self): + self.warnings = [] + self.errors = [] + self.infos = [] + self.debugs = [] + + def debug(self, msg, *args): + self.debugs.append(msg % args if args else msg) + + def info(self, msg, *args): + self.infos.append(msg % args if args else msg) + + def warning(self, msg, *args): + self.warnings.append(msg % args if args else msg) + + def error(self, msg, *args): + self.errors.append(msg % args if args else msg) + + def verbose(self, msg, *args): pass + def verboser(self, msg, *args): pass + def ridiculous(self, msg, *args): pass + + +@pytest.fixture +def mock_logger(): + """Return a fresh MockLogger for each test.""" + return MockLogger() + + +# --------------------------------------------------------------------------- +# Helper: write a file with exact binary content +# --------------------------------------------------------------------------- + +def write_binary(path, content: bytes): + """Write bytes to path, creating parent directories as needed.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + + +def _raise(exc): + """Helper for raising exceptions in lambda mocks.""" + def _inner(*args, **kwargs): + raise exc + return _inner + + +# --------------------------------------------------------------------------- +# Behavioral Tests +# --------------------------------------------------------------------------- + +class TestFindSourceRoot: + """Tests for find_source_root ascending to pyproject.toml (D-04, D-05, D-06).""" + + def test_find_source_root_happy_path(self, tmp_path): + """D-04: Returns the directory containing pyproject.toml.""" + from mlpstorage_py.submission_checker.tools.code_image import find_source_root + + root = tmp_path / "repo" + write_binary(root / "pyproject.toml", b"name = 'test'\n") + + # Test 1: Start at root + assert find_source_root(root) == root + + # Test 2: Start nested + nested = root / "a" / "b" / "c" + nested.mkdir(parents=True) + assert find_source_root(nested) == root + + def test_find_source_root_not_found(self, tmp_path): + """D-05: Raises SourceRootNotFound at filesystem root.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + find_source_root, SourceRootNotFound + ) + + # Skip if the CI environment has a pyproject.toml at / (unlikely but possible) + if (Path("/") / "pyproject.toml").exists(): + pytest.skip("Environment has pyproject.toml at filesystem root") + + # Create a path with no pyproject.toml ancestors up to filesystem root + # Actually, we can just use a deep path in tmp_path that doesn't have it. + # But we need to ensure the walk hits the real root and fails. + # Since we can't easily mock Path.parent for everything, we just use a known-isolated path. + + with pytest.raises(SourceRootNotFound, match="Could not find source root"): + find_source_root(tmp_path) + + def test_find_source_root_no_env_override(self, tmp_path, monkeypatch): + """D-06: Function does not consult environment variables.""" + from mlpstorage_py.submission_checker.tools.code_image import find_source_root + + root = tmp_path / "real_root" + write_binary(root / "pyproject.toml", b"ok\n") + + monkeypatch.setenv("MLPSTORAGE_SOURCE_ROOT", "/nonexistent") + assert find_source_root(root) == root + + +class TestCaptureCodeImage: + """Tests for capture_code_image behaviors (CAP-03, CAP-04, CAP-05, D-16..D-20).""" + + def test_capture_happy_path(self, tmp_path, mock_logger): + """CAP-03/05: Produces code/ + .code-hash.json with source tree copy.""" + from mlpstorage_py.submission_checker.tools.code_image import capture_code_image + + src = tmp_path / "src" + write_binary(src / "main.py", b"print('hi')\n") + write_binary(src / "lib" / "util.py", b"def f(): pass\n") + write_binary(src / "README.md", b"# project\n") + + image_dir = tmp_path / "out" + capture_code_image(src, image_dir, mock_logger) + + out_code = image_dir / "code" + assert out_code.is_dir() + assert (out_code / ".code-hash.json").is_file() + assert (out_code / "main.py").read_text() == "print('hi')\n" + assert (out_code / "lib" / "util.py").read_text() == "def f(): pass\n" + assert (out_code / "README.md").read_text() == "# project\n" + + def test_capture_exclusions(self, tmp_path, mock_logger): + """CAP-04, HASH-02: Excludes test/, tests/, .git/, __pycache__/, dotfiles.""" + from mlpstorage_py.submission_checker.tools.code_image import capture_code_image + + src = tmp_path / "src" + write_binary(src / "main.py", b"main\n") + write_binary(src / "test" / "conftest.py", b"test\n") + write_binary(src / "tests" / "test_foo.py", b"tests\n") + write_binary(src / ".git" / "HEAD", b"git\n") + write_binary(src / "pkg" / "__pycache__" / "mod.pyc", b"pyc\n") + write_binary(src / ".hidden", b"dotfile\n") + + image_dir = tmp_path / "out" + capture_code_image(src, image_dir, mock_logger) + + code = image_dir / "code" + assert (code / "main.py").exists() + assert not (code / "test").exists() + assert not (code / "tests").exists() + assert not (code / ".git").exists() + assert not (code / "pkg" / "__pycache__").exists() + # MD5_EXCLUDE_PREFIXES doesn't exclude all dotfiles by default, only .git/ .pytest_cache/ etc. + # But CAP-04 says "excludes dotfiles, dotdirs". + # Let's check my implementation. My ignore_logic handles prefixes. + # Actually, MD5_EXCLUDE_PREFIXES does NOT contain all dotfiles. + # If I want to match CAP-04 strictly, I might need to add more. + # But D-22 says "No changes to MD5_EXCLUDE_FILENAMES". + # I'll stick to what MD5_EXCLUDE_PREFIXES provides. + + def test_capture_recorded_hash_equals_captured_tree_hash(self, tmp_path, mock_logger): + """D-19: Recorded hash matches compute_code_tree_md5 of the output.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + capture_code_image, verify_image_self_consistent + ) + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + image_dir = tmp_path / "out" + capture_code_image(src, image_dir, mock_logger) + + assert verify_image_self_consistent(image_dir / "code", mock_logger) is True + + @pytest.mark.skipif(sys.platform == "win32", reason="os.rename atomicity semantics differ on Windows") + def test_capture_atomicity_stale_cleanup(self, tmp_path, mock_logger): + """D-17, D-18: Cleans stale code.tmp/ and is atomic.""" + from mlpstorage_py.submission_checker.tools.code_image import capture_code_image + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + out = tmp_path / "out" + stale_tmp = out / "code.tmp" + write_binary(stale_tmp / "sentinel.txt", b"garbage\n") + + capture_code_image(src, out, mock_logger) + + assert not stale_tmp.exists() + assert any("stale code.tmp/" in w for w in mock_logger.warnings) + assert (out / "code").is_dir() + + def test_capture_already_exists_raises(self, tmp_path, mock_logger): + """D-16: Never silently re-capture.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + capture_code_image, CodeImageError + ) + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + out = tmp_path / "out" + (out / "code").mkdir(parents=True) + + with pytest.raises(CodeImageError, match="[Cc]ode image already exists"): + capture_code_image(src, out, mock_logger) + + def test_capture_rejects_unknown_mlpstorage_version(self, tmp_path, mock_logger, monkeypatch): + """CAP-05 hardening: refuse to stamp degenerate mlpstorage_version="unknown" — happens when + the package isn't installed AND pyproject.toml is unreadable. Fail before any FS work.""" + from mlpstorage_py.errors import ConfigurationError + import mlpstorage_py.submission_checker.tools.code_image as code_image_mod + + monkeypatch.setattr(code_image_mod, "MLPSTORAGE_VERSION", "unknown") + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + out = tmp_path / "out" + + with pytest.raises(ConfigurationError, match="mlpstorage version could not be resolved"): + code_image_mod.capture_code_image(src, out, mock_logger) + + assert not (out / "code").exists(), "capture must not leave a partial code/ dir" + assert not (out / "code.tmp").exists(), "capture must not leave a partial code.tmp/ dir" + + +class TestLoadCodeImage: + """Tests for load_code_image behavior (D-02, D-14, D-15).""" + + def test_load_happy_path(self, tmp_path, mock_logger): + """D-02: Returns CodeImage instance from JSON.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + capture_code_image, load_code_image + ) + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + image_parent = tmp_path / "out" + capture_code_image(src, image_parent, mock_logger) + + img = load_code_image(image_parent / "code", mock_logger) + assert img.path == image_parent / "code" + assert len(img.hash) == 32 + assert img.algorithm == "md5-tree-v1" + assert img.mlpstorage_version == MLPSTORAGE_VERSION + + def test_load_missing_file_raises(self, tmp_path, mock_logger): + """D-14: MissingHashFile raised when JSON absent.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + load_code_image, MissingHashFile + ) + + path = tmp_path / "img" + path.mkdir() + + with pytest.raises(MissingHashFile, match=".code-hash.json not found"): + load_code_image(path, mock_logger) + + @pytest.mark.parametrize("payload, reason", [ + ({"bad": "json"}, "Missing required field"), + ({"hash": "a", "algorithm": "md5-tree-v1", "captured_at": "2026-01-01T00:00:00Z", "mlpstorage_version": "1", "git_sha": None}, "Invalid MD5 hash format"), + ({"hash": "a"*32, "algorithm": "v2", "captured_at": "2026-01-01T00:00:00Z", "mlpstorage_version": "1", "git_sha": None}, "Unknown algorithm"), + ({"hash": "a"*32, "algorithm": "md5-tree-v1", "captured_at": "bad", "mlpstorage_version": "1", "git_sha": None}, "Invalid captured_at"), + ({"hash": "a"*32, "algorithm": "md5-tree-v1", "captured_at": "2026-01-01T00:00:00Z", "mlpstorage_version": "1", "git_sha": "bad"}, "Invalid git_sha"), + ]) + def test_load_malformed_json_raises(self, tmp_path, mock_logger, payload, reason): + """D-15: MalformedHashFile raised for various invalid schemas.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + load_code_image, MalformedHashFile + ) + + path = tmp_path / "img" + path.mkdir() + (path / ".code-hash.json").write_text(json.dumps(payload)) + + with pytest.raises(MalformedHashFile, match=reason): + load_code_image(path, mock_logger) + + +class TestVerifySourceAgainstImage: + """Tests for verify_source_against_image (D-11, D-13).""" + + def test_verify_source_match(self, tmp_path, mock_logger): + """D-11: Returns True when source matches image.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + capture_code_image, verify_source_against_image + ) + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + image_parent = tmp_path / "out" + capture_code_image(src, image_parent, mock_logger) + + assert verify_source_against_image(src, image_parent / "code", mock_logger) is True + + def test_verify_source_mismatch(self, tmp_path, mock_logger): + """D-11: Returns False when source differs from image.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + capture_code_image, verify_source_against_image + ) + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + image_parent = tmp_path / "out" + capture_code_image(src, image_parent, mock_logger) + + # Tamper with source + write_binary(src / "a.py", b"B\n") + + assert verify_source_against_image(src, image_parent / "code", mock_logger) is False + + +class TestVerifyImageSelfConsistent: + """Tests for verify_image_self_consistent (D-12, D-13).""" + + def test_verify_image_self_match(self, tmp_path, mock_logger): + """D-12: Returns True for unmodified capture.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + capture_code_image, verify_image_self_consistent + ) + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + image_parent = tmp_path / "out" + capture_code_image(src, image_parent, mock_logger) + + assert verify_image_self_consistent(image_parent / "code", mock_logger) is True + + def test_verify_image_self_tamper(self, tmp_path, mock_logger): + """D-12: Returns False if captured tree is modified.""" + from mlpstorage_py.submission_checker.tools.code_image import ( + capture_code_image, verify_image_self_consistent + ) + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + image_parent = tmp_path / "out" + capture_code_image(src, image_parent, mock_logger) + + # Tamper with capture + write_binary(image_parent / "code" / "a.py", b"B\n") + + assert verify_image_self_consistent(image_parent / "code", mock_logger) is False + + +class TestCodeHashJsonSchema: + """Tests for .code-hash.json schema and Git SHA resolution (D-07, D-08, D-09, D-10).""" + + def test_schema_invariants(self, tmp_path, mock_logger): + """TEST-10: Verifies algorithm, captured_at, version, and hash format.""" + from mlpstorage_py.submission_checker.tools.code_image import capture_code_image + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + image_parent = tmp_path / "out" + capture_code_image(src, image_parent, mock_logger) + + payload = json.loads((image_parent / "code" / ".code-hash.json").read_text()) + + assert payload["algorithm"] == "md5-tree-v1" + assert re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", payload["captured_at"]) + assert payload["mlpstorage_version"] == MLPSTORAGE_VERSION + assert re.fullmatch(r"[0-9a-f]{32}", payload["hash"]) + + # Field order check + keys = list(payload.keys()) + expected_keys = ["hash", "algorithm", "captured_at", "mlpstorage_version", "git_sha"] + assert keys == expected_keys + + def test_git_sha_success(self, tmp_path, mock_logger, monkeypatch): + """D-08: git_sha is 40-char SHA on success.""" + from mlpstorage_py.submission_checker.tools.code_image import capture_code_image + import mlpstorage_py.submission_checker.tools.code_image as code_image_mod + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + fake_sha = "a" * 40 + def mock_run(*args, **kwargs): + return SimpleNamespace(returncode=0, stdout=fake_sha + "\n", stderr="") + + monkeypatch.setattr(code_image_mod.subprocess, "run", mock_run) + + image_parent = tmp_path / "out" + img = capture_code_image(src, image_parent, mock_logger) + assert img.git_sha == fake_sha + + @pytest.mark.parametrize("mock_fn, log_msg", [ + (_raise(FileNotFoundError("git not found")), None), + (lambda *a, **k: SimpleNamespace(returncode=128, stdout="", stderr="error"), None), + (_raise(subprocess.TimeoutExpired(["git"], 5)), "Failed to resolve git SHA"), + (lambda *a, **k: SimpleNamespace(returncode=0, stdout="short\n", stderr=""), None), + ]) + def test_git_sha_failures(self, tmp_path, mock_logger, monkeypatch, mock_fn, log_msg): + """D-08: git_sha is null on various subprocess failures.""" + from mlpstorage_py.submission_checker.tools.code_image import capture_code_image + import mlpstorage_py.submission_checker.tools.code_image as code_image_mod + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + monkeypatch.setattr(code_image_mod.subprocess, "run", mock_fn) + + image_parent = tmp_path / "out" + img = capture_code_image(src, image_parent, mock_logger) + assert img.git_sha is None + if log_msg: + assert any(log_msg in w for w in mock_logger.warnings) + + def test_git_sha_argv_spy(self, tmp_path, mock_logger, monkeypatch): + """D-08: Subprocess argv and kwargs check.""" + from mlpstorage_py.submission_checker.tools.code_image import capture_code_image + import mlpstorage_py.submission_checker.tools.code_image as code_image_mod + + src = tmp_path / "src" + write_binary(src / "a.py", b"A\n") + + calls = [] + def spy(*args, **kwargs): + calls.append((args, kwargs)) + return SimpleNamespace(returncode=0, stdout="b"*40+"\n", stderr="") + + monkeypatch.setattr(code_image_mod.subprocess, "run", spy) + + capture_code_image(src, tmp_path / "out", mock_logger) + + assert len(calls) == 1 + args, kwargs = calls[0] + assert args[0] == ["git", "rev-parse", "HEAD"] + assert kwargs["cwd"] == str(src) + assert kwargs["check"] is False + assert kwargs["timeout"] == 5 + assert kwargs.get("shell", False) is False diff --git a/mlpstorage_py/tests/test_config_reference_checksum.py b/mlpstorage_py/tests/test_config_reference_checksum.py index 2732e8c7..27ad1307 100644 --- a/mlpstorage_py/tests/test_config_reference_checksum.py +++ b/mlpstorage_py/tests/test_config_reference_checksum.py @@ -44,6 +44,8 @@ def test_md5_exclude_prefixes_membership(self): "build/", "dist/", ".tox/", + "test/", + "tests/", } for prefix in required: assert prefix in MD5_EXCLUDE_PREFIXES, f"Missing prefix: {prefix}" @@ -59,13 +61,15 @@ def test_md5_exclude_prefixes_exact_membership(self): "build/", "dist/", ".tox/", + "test/", + "tests/", ) assert MD5_EXCLUDE_PREFIXES == expected def test_md5_exclude_filenames_membership(self): """MD5_EXCLUDE_FILENAMES must be a tuple containing all required filename patterns (D-13).""" assert isinstance(MD5_EXCLUDE_FILENAMES, tuple) - required = {"*.pyc", "*.pyo", ".DS_Store", "Thumbs.db"} + required = {".code-hash.json", "*.pyc", "*.pyo", ".DS_Store", "Thumbs.db"} for pattern in required: assert pattern in MD5_EXCLUDE_FILENAMES, f"Missing pattern: {pattern}" diff --git a/mlpstorage_py/tests/test_exit_code_code_image_error.py b/mlpstorage_py/tests/test_exit_code_code_image_error.py new file mode 100644 index 00000000..50af5520 --- /dev/null +++ b/mlpstorage_py/tests/test_exit_code_code_image_error.py @@ -0,0 +1,63 @@ +"""Unit tests for EXIT_CODE.CODE_IMAGE_ERROR (Plan 02-01, Task 1). + +Phase 2 introduces a new enum member CODE_IMAGE_ERROR on EXIT_CODE, used by +the typed-exception → process-exit-code mapping in main.py to signal that a +code-image capture or verify operation failed. + +Per 02-CONTEXT.md D-22 the value is 2; per 02-PATTERNS.md the symbol is +preferred for grep-ability over reusing INVALID_ARGUMENTS at the call site. +Because IntEnum aliases on duplicate values, CODE_IMAGE_ERROR is an alias of +INVALID_ARGUMENTS — both names resolve to 2. +""" + +from mlpstorage_py.config import EXIT_CODE + + +def test_code_image_error_member_exists(): + """The new enum member is importable.""" + assert hasattr(EXIT_CODE, "CODE_IMAGE_ERROR") + + +def test_code_image_error_value_is_two(): + """Per D-22 the integer value is 2 (aliased with INVALID_ARGUMENTS).""" + assert EXIT_CODE.CODE_IMAGE_ERROR.value == 2 + + +def test_code_image_error_int_cast(): + """The member is usable as a process exit code (int-castable).""" + assert int(EXIT_CODE.CODE_IMAGE_ERROR) == 2 + + +def test_code_image_error_name_grepable(): + """Either the name resolves to CODE_IMAGE_ERROR directly, or — because + IntEnum's canonical-name resolution prefers the first-defined alias — + the symbol still exists as a class attribute. The grep-ability acceptance + criterion is that ``CODE_IMAGE_ERROR`` is a usable symbolic name. + """ + # Direct attribute access must work regardless of canonical aliasing. + assert EXIT_CODE.CODE_IMAGE_ERROR is not None + # And the symbolic identity must be the same as the INVALID_ARGUMENTS alias + # because they share the integer value 2. + assert EXIT_CODE.CODE_IMAGE_ERROR == EXIT_CODE.INVALID_ARGUMENTS + + +def test_preexisting_exit_codes_unchanged(): + """Adding the alias must not renumber pre-existing members.""" + assert EXIT_CODE.SUCCESS.value == 0 + assert EXIT_CODE.GENERAL_ERROR.value == 1 + assert EXIT_CODE.INVALID_ARGUMENTS.value == 2 + assert EXIT_CODE.FILE_NOT_FOUND.value == 3 + assert EXIT_CODE.PERMISSION_DENIED.value == 4 + assert EXIT_CODE.CONFIGURATION_ERROR.value == 5 + assert EXIT_CODE.FAILURE.value == 6 + assert EXIT_CODE.TIMEOUT.value == 7 + assert EXIT_CODE.INTERRUPTED.value == 8 + + +def test_enumeration_does_not_raise(): + """Iterating the enum produces all defined members without error.""" + members = list(EXIT_CODE) + # IntEnum aliases are not iterated as separate entries; iteration count + # should equal the count of distinct canonical values (9 in the current + # enum: SUCCESS..INTERRUPTED). + assert len(members) == 9 diff --git a/mlpstorage_py/tests/test_generate_output_location.py b/mlpstorage_py/tests/test_generate_output_location.py new file mode 100644 index 00000000..61d158b0 --- /dev/null +++ b/mlpstorage_py/tests/test_generate_output_location.py @@ -0,0 +1,344 @@ +"""Unit tests for ``generate_output_location`` and the orgname/systemname +keyword-only contract (Plan 02-01, Task 2). + +Per 02-CONTEXT.md D-03 the runtime output path is restructured so results +land under ``{results_dir}/{closed|open}//...`` (with an additional +``results//`` segment for OPEN). Per the Gemini MEDIUM +trust-contract review (02-REVIEWS.md), ``generate_output_location`` does +NOT read environment variables — it accepts ``orgname`` and ``systemname`` +as keyword-only parameters threaded by the CLI dispatch layer (Plan 02-02). + +This test file exercises: + + * the new path prefix for CLOSED and OPEN, + * the back-compat shape for ``whatif`` and any other non-{closed,open} mode, + * the typed ``ConfigurationError`` raised when the kwargs are missing for + closed/open modes (NOT a bare ``KeyError`` from a hidden env read), + * the module-level env-var-name constants + ``MLPSTORAGE_ORGNAME_ENVVAR`` / ``MLPSTORAGE_SYSTEMNAME_ENVVAR`` + exported for Plan 02-02's helper to consume as a single source of truth. +""" + +import types + +import pytest + +from mlpstorage_py.config import BENCHMARK_TYPES +from mlpstorage_py.errors import ConfigurationError + + +def _benchmark(mode: str, model: str = "unet3d", command: str = "datagen", + benchmark_type=BENCHMARK_TYPES.training, results_dir: str = "/tmp/r", + index_type: str | None = None, vdb_engine: str | None = None): + """Build a minimal benchmark stand-in with the attributes + ``generate_output_location`` reads. + + ``index_type`` is set for vector_database benchmarks; the runtime path for + that type includes a per-index_type segment so AISAQ results are kept + separate from DISKANN/HNSW (they're not comparable). The on-disk index + directory uses the UPPERCASE token (DISKANN / HNSW / AISAQ), matching + ``args.index_type`` and ``summary.json.index_type``. ``vdb_engine`` adds + the engine segment between and . + """ + args = types.SimpleNamespace( + mode=mode, + results_dir=results_dir, + model=model, + command=command, + ) + if index_type is not None: + args.index_type = index_type + if vdb_engine is not None: + args.vdb_engine = vdb_engine + return types.SimpleNamespace(args=args, BENCHMARK_TYPE=benchmark_type) + + +# --------------------------------------------------------------------------- +# Module-level constants +# --------------------------------------------------------------------------- + +def test_envvar_constants_exported(): + """The module exports the two env-var-name constants for the dispatch + helper to consume.""" + from mlpstorage_py.rules.utils import ( + MLPSTORAGE_ORGNAME_ENVVAR, + MLPSTORAGE_SYSTEMNAME_ENVVAR, + ) + + assert MLPSTORAGE_ORGNAME_ENVVAR == "MLPSTORAGE_ORGNAME" + assert MLPSTORAGE_SYSTEMNAME_ENVVAR == "MLPSTORAGE_SYSTEMNAME" + + +# --------------------------------------------------------------------------- +# CLOSED prefix +# --------------------------------------------------------------------------- + +def test_closed_training_prefix(): + """CLOSED training/// sits under + {results_dir}/closed//.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="closed") + path = generate_output_location(b, datetime_str="X", orgname="acme") + assert path.startswith("/tmp/r/closed/acme/training/unet3d/datagen/"), path + assert path.endswith("/X"), path + + +def test_closed_checkpointing_prefix(): + """CLOSED checkpointing// sits under + {results_dir}/closed//.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark( + mode="closed", + model="llama3-8b", + command="run", + benchmark_type=BENCHMARK_TYPES.checkpointing, + ) + path = generate_output_location(b, datetime_str="X", orgname="acme") + assert path.startswith("/tmp/r/closed/acme/checkpointing/llama3-8b/"), path + assert path.endswith("/X"), path + + +# --------------------------------------------------------------------------- +# OPEN prefix +# --------------------------------------------------------------------------- + +def test_open_training_prefix(): + """OPEN training prepends both closed/open-segment and + results// before the per-type tail.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="open") + path = generate_output_location( + b, datetime_str="X", orgname="acme", systemname="sys-1", + ) + assert path.startswith( + "/tmp/r/open/acme/results/sys-1/training/unet3d/datagen/" + ), path + + +def test_open_vector_database_prefix_includes_index_type(): + """vector_database results are split by engine/index_type because AISAQ + results are not comparable to DISKANN/HNSW. The runtime path includes + the / segments between and . + + On-disk type segment is `vector_database` (BENCHMARK_TYPES.name) and the + index directory is the UPPERCASE token (`DISKANN`), matching + ``args.index_type`` and ``summary.json.index_type``.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark( + mode="open", + command="run", + benchmark_type=BENCHMARK_TYPES.vector_database, + index_type="DISKANN", + vdb_engine="milvus", + ) + path = generate_output_location( + b, datetime_str="X", orgname="acme", systemname="sys-1", + ) + assert path.startswith( + "/tmp/r/open/acme/results/sys-1/vector_database/milvus/DISKANN/run/" + ), path + + +def test_closed_vector_database_prefix_includes_index_type(): + """Same contract on the CLOSED side: / sits between + and . + + The type segment is `vector_database` and the index directory is the + UPPERCASE token `AISAQ`, matching ``args.index_type``.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark( + mode="closed", + command="run", + benchmark_type=BENCHMARK_TYPES.vector_database, + index_type="AISAQ", + vdb_engine="milvus", + ) + path = generate_output_location(b, datetime_str="X", orgname="acme") + assert path.startswith( + "/tmp/r/closed/acme/vector_database/milvus/AISAQ/run/" + ), path + + +# --------------------------------------------------------------------------- +# Back-compat: whatif (and any non-{closed,open} mode) — unchanged shape +# --------------------------------------------------------------------------- + +def test_whatif_has_no_closed_open_prefix(): + """Mode=whatif keeps the legacy shape — no closed/open segment, + no orgname/systemname.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="whatif") + path = generate_output_location(b, datetime_str="X") + # No prefix segments appear. + assert "/closed/" not in path, path + assert "/open/" not in path, path + assert "/acme/" not in path, path + # Legacy shape preserved. + assert path.startswith("/tmp/r/training/unet3d/datagen/"), path + assert path.endswith("/X"), path + + +def test_missing_mode_attribute_keeps_legacy_shape(): + """If args.mode is missing entirely (older callers), the function + returns the legacy shape and does not raise.""" + from mlpstorage_py.rules.utils import generate_output_location + + args = types.SimpleNamespace(results_dir="/tmp/r", model="unet3d", command="datagen") + b = types.SimpleNamespace(args=args, BENCHMARK_TYPE=BENCHMARK_TYPES.training) + path = generate_output_location(b, datetime_str="X") + assert path == "/tmp/r/training/unet3d/datagen/X" + + +# --------------------------------------------------------------------------- +# Typed-error trust contract: missing kwargs for closed/open modes +# --------------------------------------------------------------------------- + +def test_closed_missing_orgname_raises_configuration_error(): + """CLOSED without orgname raises a typed ConfigurationError that + identifies the missing parameter.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="closed") + with pytest.raises(ConfigurationError) as exc_info: + generate_output_location(b, datetime_str="X") + # The CLI dispatch layer can recover the parameter name to surface in + # its own user-facing error. + assert exc_info.value.parameter == "orgname" + # And the suggestion text references the env-var name constant so the + # user sees actionable guidance. + assert "MLPSTORAGE_ORGNAME" in str(exc_info.value) + + +def test_closed_empty_orgname_raises_configuration_error(): + """An empty-string orgname is treated as missing (avoids producing + a path with an empty path segment).""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="closed") + with pytest.raises(ConfigurationError) as exc_info: + generate_output_location(b, datetime_str="X", orgname="") + assert exc_info.value.parameter == "orgname" + + +def test_open_missing_systemname_raises_configuration_error(): + """OPEN with orgname but no systemname raises a typed + ConfigurationError that identifies systemname as the missing + parameter.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="open") + with pytest.raises(ConfigurationError) as exc_info: + generate_output_location(b, datetime_str="X", orgname="acme") + assert exc_info.value.parameter == "systemname" + assert "MLPSTORAGE_SYSTEMNAME" in str(exc_info.value) + + +def test_open_missing_orgname_raises_configuration_error(): + """OPEN missing orgname is also a typed error — orgname is reported + first because it is the outer segment in the path.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="open") + with pytest.raises(ConfigurationError) as exc_info: + generate_output_location(b, datetime_str="X", systemname="sys-1") + assert exc_info.value.parameter == "orgname" + + +# --------------------------------------------------------------------------- +# Negative assertion: no os.environ reads for MLPSTORAGE_* names +# --------------------------------------------------------------------------- + +def test_function_does_not_read_mlpstorage_env_vars(monkeypatch): + """The function MUST NOT touch os.environ for MLPSTORAGE_* — that is the + CLI dispatch layer's job. We assert by patching the values to something + that would produce a wrong path if the function read them; the function's + explicit kwargs must win.""" + monkeypatch.setenv("MLPSTORAGE_ORGNAME", "ENV-ORGNAME-WRONG") + monkeypatch.setenv("MLPSTORAGE_SYSTEMNAME", "ENV-SYSTEMNAME-WRONG") + + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="closed") + path = generate_output_location(b, datetime_str="X", orgname="acme") + # Kwargs win: 'acme' appears, the env-var value does NOT. + assert "/closed/acme/" in path, path + assert "ENV-ORGNAME-WRONG" not in path, path + + +# --------------------------------------------------------------------------- +# Path-component safety: reject path-traversal / unsafe segments at the +# trust boundary (defense in depth — argparse choices= covers the CLI +# entrypoint; this catches programmatic callers that bypass argparse). +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("bad_orgname", [ + "../etc", # parent-dir traversal + "..", # reserved + ".", # reserved + "/absolute", # absolute reset (would clobber results_dir via os.path.join) + "acme/sub", # embedded separator + "acme\x00", # null byte + "acme name", # whitespace + "", # empty +]) +def test_orgname_rejects_unsafe_path_components(bad_orgname): + """orgname comes from MLPSTORAGE_ORGNAME (user-controlled env). The path + generator must reject anything that isn't a single safe segment, even if + the CLI dispatch layer somehow forwards it (defense in depth).""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="closed") + with pytest.raises((ValueError, ConfigurationError)): + generate_output_location(b, datetime_str="X", orgname=bad_orgname) + + +@pytest.mark.parametrize("bad_systemname", ["../etc", "..", "/absolute", "sys/sub"]) +def test_systemname_rejects_unsafe_path_components(bad_systemname): + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="open") + with pytest.raises(ValueError): + generate_output_location( + b, datetime_str="X", orgname="acme", systemname=bad_systemname, + ) + + +@pytest.mark.parametrize("bad_index", ["../etc", "..", "/absolute", "DISKANN/sub"]) +def test_vdb_index_rejects_unsafe_path_components(bad_index): + """A programmatic caller (test fixture, future internal API) that + bypasses cli.vectordb_args.validate_vectordb_arguments and feeds an + arbitrary string as args.vdb_index must NOT land in a traversal path.""" + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark( + mode="closed", + command="run", + benchmark_type=BENCHMARK_TYPES.vector_database, + index_type=bad_index, + vdb_engine="milvus", + ) + with pytest.raises(ValueError): + generate_output_location(b, datetime_str="X", orgname="acme") + + +@pytest.mark.parametrize("bad_value", ["../bad", "..", "/abs", "a/b"]) +def test_model_rejects_unsafe_path_components(bad_value): + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="closed", model=bad_value) + with pytest.raises(ValueError): + generate_output_location(b, datetime_str="X", orgname="acme") + + +def test_datetime_str_rejects_unsafe_path_components(): + from mlpstorage_py.rules.utils import generate_output_location + + b = _benchmark(mode="closed") + with pytest.raises(ValueError): + generate_output_location(b, datetime_str="../escape", orgname="acme") diff --git a/mlpstorage_py/tests/test_issue_376_file_arg_conflict.py b/mlpstorage_py/tests/test_issue_376_file_arg_conflict.py index 7809c2a4..950a8f3b 100644 --- a/mlpstorage_py/tests/test_issue_376_file_arg_conflict.py +++ b/mlpstorage_py/tests/test_issue_376_file_arg_conflict.py @@ -22,6 +22,13 @@ which is the intended behavior. These tests pin those invariants so the regression cannot reappear. + +Post-CLI-redesign note: the three-mode positional parser (closed/open/whatif) +moved the storage-type from an option (``--file`` / ``--object``) to a single +positional ``data_access_protocol`` with ``choices=['file', 'object']``. The +duplicate-registration invariant that drove issue #376 is preserved by +checking that the universal adder never registers the storage-type surface, +while the storage-type adder is the sole registrant. """ import argparse @@ -41,25 +48,24 @@ def test_universal_then_storage_type_does_not_conflict(): ) parser = argparse.ArgumentParser() - add_universal_arguments(parser) + add_universal_arguments(parser, req_results=False) # Pre-fix this line raised: # argparse.ArgumentError: argument --file: conflicting option string: --file add_storage_type_arguments(parser) - ns = parser.parse_args(["--file"]) - assert ns.file is True - assert ns.object is None + ns = parser.parse_args(["file"]) + assert ns.data_access_protocol == "file" def test_file_declared_in_exactly_one_adder(): - """``--file`` must live in add_storage_type_arguments only.""" + """The ``file``/``object`` storage-type surface must live in add_storage_type_arguments only.""" from mlpstorage_py.cli.common_args import ( add_universal_arguments, add_storage_type_arguments, ) universal_parser = argparse.ArgumentParser() - add_universal_arguments(universal_parser) + add_universal_arguments(universal_parser, req_results=False) universal_opts = { opt for action in universal_parser._actions for opt in action.option_strings } @@ -68,14 +74,24 @@ def test_file_declared_in_exactly_one_adder(): "issue #376 because every benchmark subparser then registers --file twice." ) assert "--object" not in universal_opts + universal_dests = {action.dest for action in universal_parser._actions} + assert "data_access_protocol" not in universal_dests, ( + "data_access_protocol leaked into add_universal_arguments; this would " + "re-introduce the duplicate-positional variant of issue #376." + ) storage_parser = argparse.ArgumentParser() add_storage_type_arguments(storage_parser) - storage_opts = { - opt for action in storage_parser._actions for opt in action.option_strings + storage_dests = {action.dest for action in storage_parser._actions} + storage_choices = { + choice + for action in storage_parser._actions + if action.choices + for choice in action.choices } - assert "--file" in storage_opts - assert "--object" in storage_opts + assert "data_access_protocol" in storage_dests + assert "file" in storage_choices + assert "object" in storage_choices # --------------------------------------------------------------------------- @@ -112,7 +128,7 @@ def test_benchmark_subparser_builds_without_argparse_conflict(module_path, build cmd_parser = sub_programs.add_parser(builder_attr.replace("add_", "").replace("_arguments", "")) # If this raises argparse.ArgumentError, the regression is back. - builder(cmd_parser) + builder(cmd_parser, "closed") # --------------------------------------------------------------------------- @@ -128,8 +144,8 @@ def test_debug_flag_parses_after_fix(): ) parser = argparse.ArgumentParser() - add_universal_arguments(parser) + add_universal_arguments(parser, req_results=False) add_storage_type_arguments(parser) - ns = parser.parse_args(["--file", "--debug"]) + ns = parser.parse_args(["file", "--debug"]) assert ns.debug is True diff --git a/mlpstorage_py/tests/test_main_code_image_wiring.py b/mlpstorage_py/tests/test_main_code_image_wiring.py new file mode 100644 index 00000000..f977b9fd --- /dev/null +++ b/mlpstorage_py/tests/test_main_code_image_wiring.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +""" +Tests for the Phase 2 wiring of capture_or_verify_code_image into main.py. + +Covers D-07 (insertion point) and D-22 (exit-code mapping). + +Run with: + pytest mlpstorage_py/tests/test_main_code_image_wiring.py -v +""" + +import ast +from pathlib import Path + +import pytest + +from mlpstorage_py.config import EXIT_CODE + + +MAIN_PATH = Path(__file__).resolve().parents[1] / "main.py" + + +class TestMainImports: + def test_main_imports_capture_or_verify_helper(self): + """main.py must import the helper from the Phase 1 module.""" + source = MAIN_PATH.read_text() + assert "from mlpstorage_py.submission_checker.tools.code_image import" in source, \ + "main.py should have a single import line for the code_image symbols" + assert "capture_or_verify_code_image" in source + assert "CodeImageError" in source + + def test_main_importable(self): + """Importing main must not raise (no syntax / import drift).""" + from mlpstorage_py.main import main, run_benchmark # noqa: F401 + + +class TestExceptCodeImageErrorClause: + """D-22: dedicated except CodeImageError clause returning CODE_IMAGE_ERROR.""" + + def test_except_clause_present(self): + tree = ast.parse(MAIN_PATH.read_text()) + handler_names = [ + getattr(h.type, "id", None) + for h in ast.walk(tree) + if isinstance(h, ast.ExceptHandler) and h.type + ] + assert "CodeImageError" in handler_names, \ + f"main.py must have `except CodeImageError`; saw {handler_names}" + + def test_except_clause_order_dependency_before_mlpstorage(self): + """Order: ...DependencyError -> CodeImageError -> MLPStorageException catch-all. + + CodeImageError is NOT a subclass of MLPStorageException so MRO does not + implicitly fold it in; we need an explicit clause BEFORE the catch-all. + """ + tree = ast.parse(MAIN_PATH.read_text()) + # Find the main() function and inspect its top-level except handlers. + main_fns = [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef) and n.name == "main"] + assert main_fns, "main() function not found" + main_fn = main_fns[0] + + names = [] + for node in ast.walk(main_fn): + if isinstance(node, ast.Try): + for h in node.handlers: + if isinstance(h.type, ast.Name): + names.append(h.type.id) + assert "CodeImageError" in names, names + ci_idx = names.index("CodeImageError") + mlps_idx = names.index("MLPStorageException") + # CodeImageError must come BEFORE the catch-all MLPStorageException + assert ci_idx < mlps_idx, ( + f"except CodeImageError must precede except MLPStorageException; saw order {names}" + ) + + def test_except_clause_returns_code_image_error(self): + """The new except clause must return EXIT_CODE.CODE_IMAGE_ERROR.""" + source = MAIN_PATH.read_text() + assert "EXIT_CODE.CODE_IMAGE_ERROR" in source + + def test_code_image_error_value_is_two(self): + """EXIT_CODE.CODE_IMAGE_ERROR must integer-equal 2 (D-22).""" + assert int(EXIT_CODE.CODE_IMAGE_ERROR) == 2 + + +class TestHelperInvocation: + """D-07: helper called inside run_benchmark BEFORE benchmark instantiation.""" + + def test_helper_called_in_run_benchmark(self): + source = MAIN_PATH.read_text() + # Strip comment lines so we count actual code call sites only. + code_only = "\n".join( + line for line in source.splitlines() + if not line.lstrip().startswith("#") + ) + assert "capture_or_verify_code_image(args, os.environ, logger)" in code_only, \ + "main.py must call capture_or_verify_code_image(args, os.environ, logger) in run_benchmark" + + def test_helper_call_precedes_benchmark_instantiation(self): + """The helper invocation must appear before `benchmark_class(args, ...)`.""" + source = MAIN_PATH.read_text() + helper_idx = source.find("capture_or_verify_code_image(args, os.environ, logger)") + benchmark_idx = source.find("benchmark_class(args") + assert helper_idx >= 0, "helper call site not found" + assert benchmark_idx >= 0, "benchmark_class(args, ...) instantiation site not found" + assert helper_idx < benchmark_idx, ( + "capture_or_verify_code_image must be invoked BEFORE benchmark_class(args, ...)" + ) + + def test_helper_call_wrapped_in_progress_context(self): + """D-07: invocation is wrapped in progress_context for consistent UX.""" + source = MAIN_PATH.read_text() + # Find the helper call site and check the surrounding lines. + idx = source.find("capture_or_verify_code_image(args, os.environ, logger)") + assert idx >= 0 + # Look at the preceding 400 chars for the progress_context wrapper. + window = source[max(0, idx - 400):idx] + assert "progress_context" in window, "helper invocation must be inside progress_context" + assert "Capturing or verifying code image" in window diff --git a/mlpstorage_py/tests/test_rules_coverage.py b/mlpstorage_py/tests/test_rules_coverage.py index 5057e0d7..cc7ef519 100644 --- a/mlpstorage_py/tests/test_rules_coverage.py +++ b/mlpstorage_py/tests/test_rules_coverage.py @@ -59,10 +59,14 @@ def test_every_rules_md_id_is_mapped(self): ) def test_reconcile_returns_rows_for_every_rules_md_id(self): - """Row count covers every Rules.md §2/§3/§4 ID (>= 50; current 57).""" + """Row count covers every Rules.md §2/§3/§4/§5/§6 ID. + + Pre-Phase-4 baseline was >= 50 (then 57). Phase 4 added 16 §5 IDs + via Plan 04-03's Rules.md edits, so the floor rises to >= 66. + """ result = reconcile() - assert len(result["rows"]) >= 50, ( - "Expected at least 50 rows, got {}".format(len(result["rows"])) + assert len(result["rows"]) >= 66, ( + "Expected at least 66 rows, got {}".format(len(result["rows"])) ) def test_struct_2_1_2_is_check_method_disposition(self): @@ -110,7 +114,7 @@ def test_inject_unmapped_id_returns_in_unmapped_set(self, tmp_path): """ fake_md = tmp_path / "fake.md" original = RULES_MD_PATH.read_text(encoding="utf-8") - # Locked regex: ^([234]\.\d+\.\d+)\.\s+\*\*([a-zA-Z][a-zA-Z0-9]+)\*\* + # Locked regex: ^([23456]\.\d+\.\d+)\.\s+\*\*([a-zA-Z][a-zA-Z0-9]+)\*\* fake_md.write_text( original + "\n2.1.99. **fakeRule** -- placeholder for testing\n", encoding="utf-8", diff --git a/mlpstorage_py/tests/test_stub_checks.py b/mlpstorage_py/tests/test_stub_checks.py index 3d0b336d..cd7adbeb 100644 --- a/mlpstorage_py/tests/test_stub_checks.py +++ b/mlpstorage_py/tests/test_stub_checks.py @@ -1,10 +1,9 @@ -"""Unit tests for the Rules.md §5 / §6 extension stubs. +"""Unit tests for the Rules.md §6 extension stub (KVCacheCheck). -Pins the structural invariants of ``VdbCheck`` and ``KVCacheCheck`` -(Plan 03-03 Task 4 / W-02 / CLAUDE.md "new structural checks should -have unit tests"): +Pins the structural invariants of ``KVCacheCheck`` (Plan 03-03 Task 4 / +W-02 / CLAUDE.md "new structural checks should have unit tests"): - 1. Each stub subclasses ``BaseCheck``. + 1. The stub subclasses ``BaseCheck``. 2. ``discover_rules(stub_cls) == {}`` — zero ``@rule`` bindings (D-S2; Phase 3 success criterion #2). 3. Instantiation + ``__call__`` returns ``True`` with zero errors and @@ -12,6 +11,12 @@ 4. ``init_checks`` registers exactly one method, named ``_section_unimplemented``. +Phase 4 Plan 04-02 (D-01) removed ``VdbCheck`` from this fixture: VdbCheck +is no longer a stub — Rules.md §5 was filled by upstream PR #452 and +VdbCheck now carries 16 ``@rule``-decorated methods. Rules.md §6 (KVCache) +is still empty, so ``KVCacheCheck`` continues to satisfy the stub +contract pinned here. + Run with: pytest mlpstorage_py/tests/test_stub_checks.py -v """ @@ -22,12 +27,10 @@ from mlpstorage_py.submission_checker.checks.base import BaseCheck from mlpstorage_py.submission_checker.checks.kvcache_checks import KVCacheCheck -from mlpstorage_py.submission_checker.checks.vdb_checks import VdbCheck from mlpstorage_py.submission_checker.rule_registry import discover_rules STUB_CASES = [ - pytest.param(VdbCheck, "vdb checks", id="VdbCheck"), pytest.param(KVCacheCheck, "kvcache checks", id="KVCacheCheck"), ] diff --git a/mlpstorage_py/tests/test_submission_checker_structure.py b/mlpstorage_py/tests/test_submission_checker_structure.py index 84b53ae7..15884bd5 100644 --- a/mlpstorage_py/tests/test_submission_checker_structure.py +++ b/mlpstorage_py/tests/test_submission_checker_structure.py @@ -8,6 +8,7 @@ import json import os +import shutil import pytest from pathlib import Path @@ -113,9 +114,17 @@ def test_unknown_kwarg_raises_type_error(self, tmp_path): build_submission(tmp_path, no_such_kwarg=True) def test_default_fixture_no_errors(self, tmp_path, mock_logger): - """Default fixture should produce no errors from any STRUCT check.""" + """Default fixture should produce no errors from any STRUCT check. + + Plan 02-05: now that the Plan 02-03 layered self-consistency check + walks every CLOSED leaf's code/, the fixture's pre-existing code/ must + carry a matching .code-hash.json. Populating it here keeps the + "default fixture is clean" invariant intact across the full check + suite. + """ from mlpstorage_py.tests.conftest import build_submission root = build_submission(tmp_path) + _write_valid_hash_json(root / "closed" / "Acme" / "code", mock_logger) check = _make_check(root, mock_logger) result = check() assert mock_logger.errors == [], f"Unexpected errors: {mock_logger.errors}" @@ -314,12 +323,14 @@ def test_default_fixture_passes(self, tmp_path, mock_logger): assert mock_logger.errors == [] def test_missing_code_subdir(self, tmp_path, mock_logger): + # Plan 02-05: anchor renamed to requiredSubdirectoriesClosed per the + # Plan 02-03 mode-aware refactor (D-17). from mlpstorage_py.tests.conftest import build_submission root = build_submission(tmp_path, missing_required_subdir="code") check = _make_check(root, mock_logger) result = run_one_check(check, "required_subdirectories_check", mock_logger) assert result is False - assert any("[2.1.5 requiredSubdirectories]" in m for m in mock_logger.errors) + assert any("[2.1.5 requiredSubdirectoriesClosed]" in m for m in mock_logger.errors), mock_logger.errors def test_missing_results_subdir(self, tmp_path, mock_logger): from mlpstorage_py.tests.conftest import build_submission @@ -327,7 +338,7 @@ def test_missing_results_subdir(self, tmp_path, mock_logger): check = _make_check(root, mock_logger) result = run_one_check(check, "required_subdirectories_check", mock_logger) assert result is False - assert any("[2.1.5 requiredSubdirectories]" in m for m in mock_logger.errors) + assert any("[2.1.5 requiredSubdirectoriesClosed]" in m for m in mock_logger.errors), mock_logger.errors def test_missing_systems_subdir(self, tmp_path, mock_logger): from mlpstorage_py.tests.conftest import build_submission @@ -335,7 +346,7 @@ def test_missing_systems_subdir(self, tmp_path, mock_logger): check = _make_check(root, mock_logger) result = run_one_check(check, "required_subdirectories_check", mock_logger) assert result is False - assert any("[2.1.5 requiredSubdirectories]" in m for m in mock_logger.errors) + assert any("[2.1.5 requiredSubdirectoriesClosed]" in m for m in mock_logger.errors), mock_logger.errors def test_extra_submitter_subdir(self, tmp_path, mock_logger): from mlpstorage_py.tests.conftest import build_submission @@ -343,7 +354,14 @@ def test_extra_submitter_subdir(self, tmp_path, mock_logger): check = _make_check(root, mock_logger) result = run_one_check(check, "required_subdirectories_check", mock_logger) assert result is False - assert any("[2.1.5 requiredSubdirectories]" in m for m in mock_logger.errors) + assert any("[2.1.5 requiredSubdirectoriesClosed]" in m for m in mock_logger.errors), mock_logger.errors + # Plan 02-05: the legacy "only code/results/systems allowed" literal + # was replaced by the sorted-list-repr format from Plan 02-03 Task 2. + # Assert the new CLOSED required-set rendering is present. + assert any( + "allowed: ['code', 'results', 'systems']" in m + for m in mock_logger.errors + ), mock_logger.errors def test_dotfile_at_submitter_level_is_ignored(self, tmp_path, mock_logger): """Dot-prefixed entries (.DS_Store, .cache/) under closed// @@ -377,9 +395,10 @@ def test_wrapping_hint_when_submission_nested_one_level_deep(self, tmp_path, moc check = _make_check(root, mock_logger) result = run_one_check(check, "required_subdirectories_check", mock_logger) assert result is False + # Plan 02-05: anchor renamed per Plan 02-03 D-17 mode-aware refactor. wrapping_msgs = [ m for m in mock_logger.errors - if "[2.1.5 requiredSubdirectories]" in m + if "[2.1.5 requiredSubdirectoriesClosed]" in m and "nested one level deeper than expected" in m ] assert len(wrapping_msgs) == 1, mock_logger.errors @@ -392,9 +411,15 @@ def test_wrapping_hint_when_submission_nested_one_level_deep(self, tmp_path, moc class TestStruct06_CodeDirectoryContents: def test_default_fixture_passes_with_unset_reference(self, tmp_path, mock_logger): - """No reference checksum → warn ONCE (not per-submitter) and return True (D-12).""" + """No reference checksum → warn ONCE (not per-submitter) and return True (D-12). + + Plan 02-05: now that Plan 02-03's layered self-consistency check runs + unconditionally for every leaf, the fixture's pre-existing code/ must + carry a matching .code-hash.json — written via _write_valid_hash_json. + """ from mlpstorage_py.tests.conftest import build_submission root = build_submission(tmp_path) + _write_valid_hash_json(root / "closed" / "Acme" / "code", mock_logger) check = _make_check(root, mock_logger) # no ref_checksum result = run_one_check(check, "code_directory_contents_check", mock_logger) assert result is True @@ -403,39 +428,73 @@ def test_default_fixture_passes_with_unset_reference(self, tmp_path, mock_logger assert mock_logger.errors == [] def test_unset_reference_emits_single_warning_for_multi_submitter_tree(self, tmp_path, mock_logger): - """Regression for pre-fix per-submitter warning spam: 5-submitter merged - tree must emit exactly one no-checksum warning, not five.""" + """Regression for pre-fix per-submitter warning spam: multi-submitter + merged tree must emit exactly one no-checksum warning, not one per + submitter. + + Plan 02-05: populate code/ + .code-hash.json under EVERY submitter so + the layered self-consistency check passes for each leaf. STRUCT-06 now + walks every submitter under closed/ and flags missing code/ — the + fixture's AlsoAcme submitter is bare, so we manually build a minimal + code/ under it. + """ from mlpstorage_py.tests.conftest import build_submission root = build_submission(tmp_path, multiple_submitters_in_closed=True) + _write_valid_hash_json(root / "closed" / "Acme" / "code", mock_logger) + # AlsoAcme has no code/ subdirectory in the fixture; build one and + # populate the hash so the layered self-check is satisfied per-leaf. + also_code = root / "closed" / "AlsoAcme" / "code" + also_code.mkdir(parents=True) + (also_code / "mod.py").write_bytes(b"# mod\n") + _write_valid_hash_json(also_code, mock_logger) check = _make_check(root, mock_logger) # no ref_checksum result = run_one_check(check, "code_directory_contents_check", mock_logger) - assert result is True + assert result is True, mock_logger.errors warnings = [w for w in mock_logger.warnings if "[2.1.6 codeDirectoryContents]" in w] assert len(warnings) == 1, warnings def test_reference_checksum_mismatch_fails(self, tmp_path, mock_logger): - """Deliberate mismatch: zeros as reference → check fails.""" + """Deliberate mismatch: zeros as reference → check fails. + + Plan 02-05: populate .code-hash.json so the SELF-consistency check passes + — the REFERENCE_CHECKSUMS mismatch is what fails the test (not the new + layered self-check). + """ from mlpstorage_py.tests.conftest import build_submission root = build_submission(tmp_path) + _write_valid_hash_json(root / "closed" / "Acme" / "code", mock_logger) check = _make_check(root, mock_logger, ref_checksum="0" * 32) result = run_one_check(check, "code_directory_contents_check", mock_logger) assert result is False assert any("[2.1.6 codeDirectoryContents]" in m for m in mock_logger.errors) def test_reference_checksum_match_passes(self, tmp_path, mock_logger): - """Correct reference checksum → check passes silently.""" + """Correct reference checksum → check passes silently. + + Plan 02-05: populate .code-hash.json so BOTH layered checks pass + (self-consistency AND REFERENCE_CHECKSUMS upstream-identity). + """ from mlpstorage_py.tests.conftest import build_submission from mlpstorage_py.submission_checker.tools.code_checksum import compute_code_tree_md5 root = build_submission(tmp_path) code_path = str(root / "closed" / "Acme" / "code") + # Compute hash BEFORE writing .code-hash.json (the JSON file is on the + # exclude list so its presence does not affect the tree hash). actual_hash = compute_code_tree_md5(code_path, mock_logger) + _write_valid_hash_json(root / "closed" / "Acme" / "code", mock_logger) check = _make_check(root, mock_logger, ref_checksum=actual_hash) result = run_one_check(check, "code_directory_contents_check", mock_logger) assert result is True assert mock_logger.errors == [] def test_mutated_code_fails(self, tmp_path, mock_logger): - """Extra file in code/ changes hash → violation.""" + """Extra file in code/ changes hash → violation. + + Plan 02-05: mutation breaks BOTH the layered self-consistency check + (no .code-hash.json present) AND the REFERENCE_CHECKSUMS check. The + assertion now allows multiple [2.1.6] violations (count >= 1) since + both sub-paths fire — see Plan 02-05 Task 2 `` notes. + """ from mlpstorage_py.tests.conftest import build_submission from mlpstorage_py.submission_checker.tools.code_checksum import compute_code_tree_md5 # First build clean tree to get reference hash @@ -443,15 +502,24 @@ def test_mutated_code_fails(self, tmp_path, mock_logger): code_path = str(clean_root / "closed" / "Acme" / "code") clean_hash = compute_code_tree_md5(code_path, mock_logger) - # Now build mutated tree + # Now build mutated tree. Deliberately do NOT populate .code-hash.json + # — mutation breaks the hash by design, so the layered self-check is + # expected to fire alongside the REFERENCE_CHECKSUMS mismatch. root = build_submission(tmp_path / "mutated", mutate_code=True) check = _make_check(root, mock_logger, ref_checksum=clean_hash) result = run_one_check(check, "code_directory_contents_check", mock_logger) assert result is False - assert any("[2.1.6 codeDirectoryContents]" in m for m in mock_logger.errors) + # Allow multiple [2.1.6] violations (self-check + ref mismatch). + assert sum("[2.1.6 codeDirectoryContents]" in m for m in mock_logger.errors) >= 1, mock_logger.errors def test_pycache_excluded_passes(self, tmp_path, mock_logger): - """__pycache__ is excluded from hash — code_with_pycache fixture still passes.""" + """__pycache__ is excluded from hash — code_with_pycache fixture still passes. + + Plan 02-05: populate .code-hash.json AFTER the pycache fixture is built + so the recorded hash reflects the pycache-augmented (but pycache-excluded) + tree state. Both the layered self-check AND REFERENCE_CHECKSUMS must + agree. + """ from mlpstorage_py.tests.conftest import build_submission from mlpstorage_py.submission_checker.tools.code_checksum import compute_code_tree_md5 # Get clean hash @@ -460,12 +528,637 @@ def test_pycache_excluded_passes(self, tmp_path, mock_logger): clean_hash = compute_code_tree_md5(code_path, mock_logger) root = build_submission(tmp_path / "pycache", code_with_pycache=True) + _write_valid_hash_json(root / "closed" / "Acme" / "code", mock_logger) check = _make_check(root, mock_logger, ref_checksum=clean_hash) result = run_one_check(check, "code_directory_contents_check", mock_logger) assert result is True assert mock_logger.errors == [] +# --------------------------------------------------------------------------- +# Phase 2 Plan 02-03 — Helpers + Tests for the refactored +# code_directory_contents_check (VALS-01..04 + D-11 layered model + D-15 walk) +# --------------------------------------------------------------------------- + +def _write_valid_hash_json(code_path, mock_logger, **overrides): + """Compute the current tree hash and write a matching .code-hash.json. + + This makes the captured tree self-consistent so that + verify_image_self_consistent returns True without re-running + capture_code_image (which would copy the live source tree). + """ + from mlpstorage_py.submission_checker.tools.code_checksum import ( + compute_code_tree_md5, + ) + digest = compute_code_tree_md5(str(code_path), mock_logger) + payload = { + "hash": digest, + "algorithm": "md5-tree-v1", + "captured_at": "2026-06-17T00:00:00Z", + "mlpstorage_version": "3.0.9", + "git_sha": None, + } + payload.update(overrides) + hash_file = Path(code_path) / ".code-hash.json" + hash_file.write_text(json.dumps(payload)) + return payload["hash"] + + +def _make_open_leaf(root, submitter="Acme", sys_name="sys-1", wtype="training", + model="unet3d", write_code=True): + """Build a minimal open//results////code tree. + + Returns the absolute path to .../code (whether or not write_code created it). + """ + leaf = root / "open" / submitter / "results" / sys_name / wtype / model + leaf.mkdir(parents=True, exist_ok=True) + code_path = leaf / "code" + if write_code: + code_path.mkdir(parents=True, exist_ok=True) + (code_path / "mod.py").write_bytes(b"# mod\n") + (code_path / "helper.py").write_bytes(b"# helper\n") + return code_path + + +class TestStruct06_RefactoredCodeDirectoryContents: + """Refactored STRUCT-06 enforcing VALS-01..04 across CLOSED + OPEN. + + Plan 02-03: code_directory_contents_check walks both divisions and + emits separate violations for missing-code/ vs hash-mismatch (D-14), + runs REFERENCE_CHECKSUMS only for CLOSED leaves (D-11), and runs + per-tree self-consistency for both CLOSED and OPEN. + """ + + # ----- VALS-01 — CLOSED missing code/ ----- + def test_vals01_closed_missing_code_emits_missing_violation(self, tmp_path, mock_logger): + # Tree: closed/Acme/{results,systems} but no closed/Acme/code/ + sub = tmp_path / "closed" / "Acme" + (sub / "results").mkdir(parents=True) + (sub / "systems").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + missing_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "required code/ directory missing at" in m + and "closed/Acme/code" in m + ] + assert len(missing_msgs) == 1, mock_logger.errors + + # ----- VALS-02 — CLOSED self-consistency mismatch ----- + def test_vals02_closed_self_consistency_mismatch(self, tmp_path, mock_logger): + sub = tmp_path / "closed" / "Acme" + code_path = sub / "code" + code_path.mkdir(parents=True) + (code_path / "mod.py").write_bytes(b"# original\n") + _write_valid_hash_json(code_path, mock_logger) + # Mutate the tree so the hash no longer matches the recorded JSON + (code_path / "mod.py").write_bytes(b"# TAMPERED\n") + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + mismatch_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "code tree hash does not match .code-hash.json at" in m + ] + assert len(mismatch_msgs) == 1, mock_logger.errors + + # ----- VALS-02 — missing .code-hash.json ----- + def test_vals02_missing_hash_json_emits_violation(self, tmp_path, mock_logger): + sub = tmp_path / "closed" / "Acme" + code_path = sub / "code" + code_path.mkdir(parents=True) + (code_path / "mod.py").write_bytes(b"# mod\n") + # Intentionally do NOT write .code-hash.json + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + # The MissingHashFile exception message is logged as the violation msg. + any_violation = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + ] + assert len(any_violation) >= 1, mock_logger.errors + + # ----- VALS-03 — OPEN missing code/ ----- + def test_vals03_open_missing_code_emits_missing_violation(self, tmp_path, mock_logger): + # build OPEN leaf without code/ + _make_open_leaf(tmp_path, write_code=False) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + missing_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "required code/ directory missing at" in m + and "open/Acme/results/sys-1/training/unet3d/code" in m + ] + assert len(missing_msgs) == 1, mock_logger.errors + + # ----- VALS-04 — OPEN self-consistency mismatch ----- + def test_vals04_open_self_consistency_mismatch(self, tmp_path, mock_logger): + code_path = _make_open_leaf(tmp_path, write_code=True) + _write_valid_hash_json(code_path, mock_logger) + (code_path / "mod.py").write_bytes(b"# TAMPERED\n") + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + mismatch_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "code tree hash does not match .code-hash.json at" in m + ] + assert len(mismatch_msgs) == 1, mock_logger.errors + + # ----- D-11 layered model (CLOSED happy path) ----- + def test_d11_closed_layered_happy_path(self, tmp_path, mock_logger): + """When REFERENCE_CHECKSUMS matches AND self-consistency passes → True.""" + sub = tmp_path / "closed" / "Acme" + code_path = sub / "code" + code_path.mkdir(parents=True) + (code_path / "mod.py").write_bytes(b"# mod\n") + actual_hash = _write_valid_hash_json(code_path, mock_logger) + check = _make_check(tmp_path, mock_logger, ref_checksum=actual_hash) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is True, mock_logger.errors + assert mock_logger.errors == [] + + # ----- D-11 layered model (CLOSED self-consistency passes, ref mismatch) ----- + def test_d11_closed_self_consistent_but_ref_mismatch(self, tmp_path, mock_logger): + sub = tmp_path / "closed" / "Acme" + code_path = sub / "code" + code_path.mkdir(parents=True) + (code_path / "mod.py").write_bytes(b"# mod\n") + _write_valid_hash_json(code_path, mock_logger) # self-consistent + check = _make_check(tmp_path, mock_logger, ref_checksum="0" * 32) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + ref_mismatch_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "code tree MD5 mismatch: expected" in m + ] + assert len(ref_mismatch_msgs) == 1, mock_logger.errors + + # ----- D-12 single-warning preserved with new addendum ----- + def test_d12_unconfigured_warning_runs_self_consistency_with_addendum( + self, tmp_path, mock_logger + ): + sub = tmp_path / "closed" / "Acme" + code_path = sub / "code" + code_path.mkdir(parents=True) + (code_path / "mod.py").write_bytes(b"# mod\n") + _write_valid_hash_json(code_path, mock_logger) + check = _make_check(tmp_path, mock_logger) # no ref_checksum + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is True + warnings = [ + w for w in mock_logger.warnings + if "[2.1.6 codeDirectoryContents]" in w + and "reference checksum not configured" in w + and "self-consistency check still ran" in w + ] + assert len(warnings) == 1, mock_logger.warnings + + # ----- OPEN-only tree does not emit the "not configured" warning ----- + def test_open_only_tree_does_not_emit_unconfigured_warning(self, tmp_path, mock_logger): + code_path = _make_open_leaf(tmp_path, write_code=True) + _write_valid_hash_json(code_path, mock_logger) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is True, mock_logger.errors + # No "reference checksum not configured" warning when only open/ exists. + warnings = [ + w for w in mock_logger.warnings + if "reference checksum not configured" in w + ] + assert warnings == [], warnings + + # ----- D-15 walk hygiene: empty type subtree yields nothing ----- + def test_d15_walk_hygiene_no_model_yields_no_violation(self, tmp_path, mock_logger): + # open/Acme/results/sys-1/training/ exists but no model/ subdirs. + (tmp_path / "open" / "Acme" / "results" / "sys-1" / "training").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is True, mock_logger.errors + missing_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "required code/ directory missing" in m + ] + assert missing_msgs == [], missing_msgs + + +# --------------------------------------------------------------------------- +# Phase 2 Plan 02-05 — Targeted OPEN-walk tests for VALS-03/04 +# (TestStruct06_OpenCodeDirectory) +# --------------------------------------------------------------------------- + +class TestStruct06_OpenCodeDirectory: + """VALS-03 / VALS-04: STRUCT-06 walks OPEN per-leaf code/ dirs via + `_iter_open_code_dirs` (Plan 02-03 D-15) and emits per-leaf violations. + + These tests target the OPEN walk in isolation: missing per-leaf code/, + self-consistency mismatch, missing .code-hash.json, multi-leaf violation + counting, and the OPEN-only "no closed-warning" invariant. + """ + + # ----- VALS-03 — missing OPEN code/ ----- + def test_missing_open_code_dir_fails(self, tmp_path, mock_logger): + _make_open_leaf(tmp_path, write_code=False) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + missing_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "required code/ directory missing at" in m + and m.rstrip().endswith("/training/unet3d/code") + ] + assert len(missing_msgs) == 1, mock_logger.errors + + # ----- VALS-04 happy path — OPEN code/ self-consistency passes ----- + def test_present_open_code_dir_self_consistency_passes(self, tmp_path, mock_logger): + code_path = _make_open_leaf(tmp_path, write_code=True) + _write_valid_hash_json(code_path, mock_logger) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is True, mock_logger.errors + # No [2.1.6] violations. + v216 = [m for m in mock_logger.errors if "[2.1.6 codeDirectoryContents]" in m] + assert v216 == [], v216 + + # ----- VALS-04 sad path — OPEN code/ hash mismatch ----- + def test_open_code_dir_hash_mismatch_fails(self, tmp_path, mock_logger): + code_path = _make_open_leaf(tmp_path, write_code=True) + # Record a deliberately incorrect hash (32 hex zeros) in .code-hash.json. + _write_valid_hash_json(code_path, mock_logger, hash="0" * 32) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + mismatch_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "code tree hash does not match .code-hash.json at" in m + ] + assert len(mismatch_msgs) == 1, mock_logger.errors + + # ----- VALS-04 missing JSON — OPEN code/ without .code-hash.json ----- + def test_open_missing_code_hash_json_fails(self, tmp_path, mock_logger): + _make_open_leaf(tmp_path, write_code=True) + # Deliberately do NOT call _write_valid_hash_json. + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + # The MissingHashFile exception text is logged as the violation msg. + any_violation = [m for m in mock_logger.errors if "[2.1.6 codeDirectoryContents]" in m] + assert len(any_violation) >= 1, mock_logger.errors + + # ----- OPEN-only tree must not emit the closed-specific "not configured" warning ----- + def test_open_no_reference_warning_when_only_open_present(self, tmp_path, mock_logger): + code_path = _make_open_leaf(tmp_path, write_code=True) + _write_valid_hash_json(code_path, mock_logger) + check = _make_check(tmp_path, mock_logger) # no ref_checksum + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is True, mock_logger.errors + # The "reference checksum not configured" warning is CLOSED-specific. + unconfigured = [ + w for w in mock_logger.warnings + if "reference checksum not configured" in w + ] + assert unconfigured == [], unconfigured + + # ----- vector_database: per- leaf (NOT comparable across types) ----- + def test_open_vector_database_code_dir_at_index_type_level(self, tmp_path, mock_logger): + """vector_database results split by index_type because AISAQ results are + not comparable to DISKANN/HNSW. _iter_open_code_dirs must walk down to + the level (same 3-level walk as training/checkpointing's + level), yielding results//vector_database//code/. + The index directory is the UPPERCASE token (here `DISKANN`).""" + leaf = ( + tmp_path / "open" / "Acme" / "results" / "sys-1" + / "vector_database" / "DISKANN" + ) + leaf.mkdir(parents=True) + code_path = leaf / "code" + code_path.mkdir() + (code_path / "mod.py").write_bytes(b"# vdb\n") + _write_valid_hash_json(code_path, mock_logger) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is True, mock_logger.errors + # And the missing variant: vector_database// with no code/ + # must emit a missing-code violation at the index_type level. + shutil.rmtree(code_path) + mock_logger.errors.clear() + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + missing_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "required code/ directory missing at" in m + and m.rstrip().endswith("/vector_database/DISKANN/code") + ] + assert len(missing_msgs) == 1, mock_logger.errors + + # ----- kv_cache: transitional per-type (no level) ----- + def test_open_kv_cache_code_dir_at_type_level(self, tmp_path, mock_logger): + """Same contract as vector_database: kv_cache's runtime output omits the + level (writes ///), so the captured + code/ lives at results//kv_cache/code/.""" + leaf = tmp_path / "open" / "Acme" / "results" / "sys-1" / "kv_cache" + leaf.mkdir(parents=True) + code_path = leaf / "code" + code_path.mkdir() + (code_path / "mod.py").write_bytes(b"# kvcache\n") + _write_valid_hash_json(code_path, mock_logger) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is True, mock_logger.errors + + # ----- Multiple OPEN model leaves each get their own per-leaf violation ----- + def test_open_multiple_models_each_get_their_own_violation(self, tmp_path, mock_logger): + # Build TWO OPEN model leaves, both missing code/. + _make_open_leaf(tmp_path, model="unet3d", write_code=False) + _make_open_leaf(tmp_path, model="resnet50", write_code=False) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "code_directory_contents_check", mock_logger) + assert result is False + missing_msgs = [ + m for m in mock_logger.errors + if "[2.1.6 codeDirectoryContents]" in m + and "required code/ directory missing at" in m + ] + # Exactly two — one per model leaf. + assert len(missing_msgs) == 2, missing_msgs + + +# --------------------------------------------------------------------------- +# Phase 2 Plan 02-03 — Tests for mode-aware required_subdirectories_check +# (STRUCT-05 per Rules.md §2.1.5 split — D-17) +# --------------------------------------------------------------------------- + +class TestStruct05_ModeAwareRequiredSubdirectories: + """STRUCT-05 (Plan 02-03 mode-aware refactor). + + CLOSED submitter dir requires {code, results, systems}; + OPEN submitter dir requires {results, systems}; code/ lives per-leaf in OPEN. + Violation messages route through `requiredSubdirectoriesClosed` / `requiredSubdirectoriesOpen`. + """ + + def test_closed_happy_path_unchanged(self, tmp_path, mock_logger): + sub = tmp_path / "closed" / "Acme" + (sub / "code").mkdir(parents=True) + (sub / "results").mkdir(parents=True) + (sub / "systems").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "required_subdirectories_check", mock_logger) + assert result is True, mock_logger.errors + assert mock_logger.errors == [] + + def test_closed_missing_code_routes_through_closed_anchor(self, tmp_path, mock_logger): + sub = tmp_path / "closed" / "Acme" + (sub / "results").mkdir(parents=True) + (sub / "systems").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "required_subdirectories_check", mock_logger) + assert result is False + closed_anchor_msgs = [ + m for m in mock_logger.errors + if "[2.1.5 requiredSubdirectoriesClosed]" in m + and "required subdirectory 'code' missing from closed/Acme" in m + ] + assert len(closed_anchor_msgs) == 1, mock_logger.errors + + def test_open_happy_path_two_subdirs(self, tmp_path, mock_logger): + """OPEN submitter dir with {results, systems} only must pass. + + This is the Gemini-HIGH regression target — without the mode-aware + check, every OPEN package the new runtime produces would be flagged. + """ + sub = tmp_path / "open" / "Acme" + (sub / "results").mkdir(parents=True) + (sub / "systems").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "required_subdirectories_check", mock_logger) + assert result is True, mock_logger.errors + assert mock_logger.errors == [] + + def test_open_with_code_at_submitter_level_is_unexpected(self, tmp_path, mock_logger): + sub = tmp_path / "open" / "Acme" + (sub / "code").mkdir(parents=True) + (sub / "results").mkdir(parents=True) + (sub / "systems").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "required_subdirectories_check", mock_logger) + assert result is False + unexpected_msgs = [ + m for m in mock_logger.errors + if "[2.1.5 requiredSubdirectoriesOpen]" in m + and "unexpected subdirectory 'code'" in m + ] + assert len(unexpected_msgs) == 1, mock_logger.errors + + def test_open_missing_results(self, tmp_path, mock_logger): + sub = tmp_path / "open" / "Acme" + (sub / "systems").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "required_subdirectories_check", mock_logger) + assert result is False + missing_msgs = [ + m for m in mock_logger.errors + if "[2.1.5 requiredSubdirectoriesOpen]" in m + and "required subdirectory 'results' missing from open/Acme" in m + ] + assert len(missing_msgs) == 1, mock_logger.errors + + def test_open_missing_systems(self, tmp_path, mock_logger): + sub = tmp_path / "open" / "Acme" + (sub / "results").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "required_subdirectories_check", mock_logger) + assert result is False + missing_msgs = [ + m for m in mock_logger.errors + if "[2.1.5 requiredSubdirectoriesOpen]" in m + and "required subdirectory 'systems' missing from open/Acme" in m + ] + assert len(missing_msgs) == 1, mock_logger.errors + + def test_closed_wrapping_hint_still_works(self, tmp_path, mock_logger): + sub = tmp_path / "closed" / "Acme" + wrapper = sub / "benchmarks" + (wrapper / "code").mkdir(parents=True) + (wrapper / "results").mkdir(parents=True) + (wrapper / "systems").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "required_subdirectories_check", mock_logger) + assert result is False + hint_msgs = [ + m for m in mock_logger.errors + if "[2.1.5 requiredSubdirectoriesClosed]" in m + and "nested one level deeper than expected" in m + ] + assert len(hint_msgs) == 1, mock_logger.errors + + def test_open_wrapping_hint(self, tmp_path, mock_logger): + sub = tmp_path / "open" / "Acme" + wrapper = sub / "benchmarks" + (wrapper / "results").mkdir(parents=True) + (wrapper / "systems").mkdir(parents=True) + check = _make_check(tmp_path, mock_logger) + result = run_one_check(check, "required_subdirectories_check", mock_logger) + assert result is False + hint_msgs = [ + m for m in mock_logger.errors + if "[2.1.5 requiredSubdirectoriesOpen]" in m + and "nested one level deeper than expected" in m + ] + assert len(hint_msgs) == 1, mock_logger.errors + + +# --------------------------------------------------------------------------- +# Phase 2 Plan 02-05 — TestStruct05_OpenSubmitter +# Mode-aware required_subdirectories_check (TEST-11) +# Regression suite for the Gemini HIGH cross-plan finding (REVIEWS.md). +# --------------------------------------------------------------------------- + +def _build_minimal_open_submitter(root, submitter, *, with_code=False, + with_results=True, with_systems=True): + """Build a minimal open//{code?,results?,systems?}/ tree.""" + sub = os.path.join(root, "open", submitter) + os.makedirs(sub, exist_ok=True) + if with_code: + os.makedirs(os.path.join(sub, "code"), exist_ok=True) + if with_results: + os.makedirs(os.path.join(sub, "results"), exist_ok=True) + if with_systems: + os.makedirs(os.path.join(sub, "systems"), exist_ok=True) + return sub + + +def _build_minimal_closed_submitter(root, submitter, *, with_code=True, + with_results=True, with_systems=True): + """Build a minimal closed//{code?,results?,systems?}/ tree.""" + sub = os.path.join(root, "closed", submitter) + os.makedirs(sub, exist_ok=True) + if with_code: + os.makedirs(os.path.join(sub, "code"), exist_ok=True) + if with_results: + os.makedirs(os.path.join(sub, "results"), exist_ok=True) + if with_systems: + os.makedirs(os.path.join(sub, "systems"), exist_ok=True) + return sub + + +class TestStruct05_OpenSubmitter: + """Mode-aware required_subdirectories_check per Plan 02-03 Task 2 (D-17). + + Regression suite for the Gemini HIGH cross-plan finding (REVIEWS.md): + before the mode-aware refactor, EVERY OPEN submission would have been + flagged as having a missing code/ at the submitter level. These tests + directly exercise the new sub-rule anchors `requiredSubdirectoriesClosed` + and `requiredSubdirectoriesOpen` and the new "allowed: [...]" violation + message format from Plan 02-03 Task 2. + """ + + def test_closed_required_set_unchanged(self, tmp_path, mock_logger): + """CLOSED no-regression: {code, results, systems} still required.""" + _build_minimal_closed_submitter(str(tmp_path), "Acme") + check = _make_check(str(tmp_path), mock_logger) + run_one_check(check, "required_subdirectories_check", mock_logger) + # No 2.1.5 violations under EITHER anchor. + v25 = [m for m in mock_logger.errors if "[2.1.5 " in m] + assert v25 == [], v25 + + def test_open_happy_path_results_systems_passes(self, tmp_path, mock_logger): + """KEY TEST — Gemini HIGH regression target. + + OPEN submitter with {results, systems} only (no code/ at submitter + level) must pass STRUCT-05. Without the mode-aware refactor, this + would have been flagged with "required subdirectory 'code' missing". + """ + _build_minimal_open_submitter(str(tmp_path), "Acme", with_code=False) + check = _make_check(str(tmp_path), mock_logger) + run_one_check(check, "required_subdirectories_check", mock_logger) + v25 = [m for m in mock_logger.errors if "[2.1.5 " in m] + assert v25 == [], v25 + + def test_open_with_code_at_submitter_level_flags_unexpected(self, tmp_path, mock_logger): + """OPEN with code/ at submitter level → unexpected violation routed + through requiredSubdirectoriesOpen with the new "allowed: [...]" + message format. + """ + _build_minimal_open_submitter(str(tmp_path), "Acme", with_code=True) + check = _make_check(str(tmp_path), mock_logger) + run_one_check(check, "required_subdirectories_check", mock_logger) + v25 = [m for m in mock_logger.errors if "[2.1.5 " in m] + assert len(v25) == 1, v25 + assert "unexpected subdirectory 'code' in open/Acme" in v25[0] + assert "requiredSubdirectoriesOpen" in v25[0] + assert "allowed: ['results', 'systems']" in v25[0] + + def test_open_missing_results_fails(self, tmp_path, mock_logger): + _build_minimal_open_submitter( + str(tmp_path), "Acme", + with_code=False, with_results=False, with_systems=True, + ) + check = _make_check(str(tmp_path), mock_logger) + run_one_check(check, "required_subdirectories_check", mock_logger) + v25 = [m for m in mock_logger.errors if "[2.1.5 " in m] + assert any( + "required subdirectory 'results' missing from open/Acme" in m + for m in v25 + ), v25 + assert any("requiredSubdirectoriesOpen" in m for m in v25), v25 + + def test_open_missing_systems_fails(self, tmp_path, mock_logger): + _build_minimal_open_submitter( + str(tmp_path), "Acme", + with_code=False, with_results=True, with_systems=False, + ) + check = _make_check(str(tmp_path), mock_logger) + run_one_check(check, "required_subdirectories_check", mock_logger) + v25 = [m for m in mock_logger.errors if "[2.1.5 " in m] + assert any( + "required subdirectory 'systems' missing from open/Acme" in m + for m in v25 + ), v25 + assert any("requiredSubdirectoriesOpen" in m for m in v25), v25 + + def test_closed_missing_code_routes_through_closed_anchor(self, tmp_path, mock_logger): + """CLOSED missing code/ routes through requiredSubdirectoriesClosed.""" + _build_minimal_closed_submitter(str(tmp_path), "Acme", with_code=False) + check = _make_check(str(tmp_path), mock_logger) + run_one_check(check, "required_subdirectories_check", mock_logger) + v25 = [m for m in mock_logger.errors if "[2.1.5 " in m] + assert any( + "required subdirectory 'code' missing from closed/Acme" in m + for m in v25 + ), v25 + assert any("requiredSubdirectoriesClosed" in m for m in v25), v25 + + def test_open_nesting_hint_works(self, tmp_path, mock_logger): + """open/Acme/benchmarks/{results,systems} — nested one level too deep. + + The wrapping-hint diagnostic mentions the OPEN required-set elements. + """ + root = str(tmp_path) + sub = os.path.join(root, "open", "Acme") + wrap = os.path.join(sub, "benchmarks") + os.makedirs(os.path.join(wrap, "results"), exist_ok=True) + os.makedirs(os.path.join(wrap, "systems"), exist_ok=True) + check = _make_check(root, mock_logger) + run_one_check(check, "required_subdirectories_check", mock_logger) + v25 = [m for m in mock_logger.errors if "[2.1.5 " in m] + assert any( + "the submission appears to be nested one level deeper than expected" in m + for m in v25 + ), v25 + + # --------------------------------------------------------------------------- # TestStruct07_SystemsDirectoryFiles (STRUCT-07, rule 2.1.7) # --------------------------------------------------------------------------- diff --git a/mlpstorage_py/tests/test_training_check_3_6_1.py b/mlpstorage_py/tests/test_training_check_3_6_1.py new file mode 100644 index 00000000..6c986389 --- /dev/null +++ b/mlpstorage_py/tests/test_training_check_3_6_1.py @@ -0,0 +1,207 @@ +"""End-to-end tests for TrainingCheck.closed_submission_checksum (§3.6.1). + +Plan 04-02 rewrote §3.6.1 from a TODO-stub to a real delegation to +``helpers._check_code_image_layered`` — the same helper VdbCheck §5.6.1 +calls. This file is the CD-04 dedup cross-check: the SAME helper is +exercised through a DIFFERENT rule ID, and the violation messages must +carry the §3.6.1 / trainingClosedSubmissionChecksum tag, not the §5.6.1 +or §2.1.6 tag. The 5.6.1 side is locked down by test_vdb_checks.py. +""" + +import json +import os +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from mlpstorage_py.submission_checker.checks.training_checks import TrainingCheck +from mlpstorage_py.submission_checker.configuration.configuration import Config +from mlpstorage_py.submission_checker.loader import LoaderMetadata, SubmissionLogs +from mlpstorage_py.submission_checker.tools.code_image import capture_code_image + + +# --------------------------------------------------------------------------- +# Fixture helpers +# --------------------------------------------------------------------------- + +def _build_training_leaf( + tmp_path: Path, + division: str, + orgname: str, + system: str, + *, + model: str = "unet3d", + with_code_image: bool = False, +) -> Path: + """Synthesize a training submission tree under tmp_path. + + Shape: + ///results//training// + [/code/.code-hash.json when with_code_image] + + Returns the per-leaf training path. + """ + leaf = ( + tmp_path + / division + / orgname + / "results" + / system + / "training" + / model + ) + leaf.mkdir(parents=True, exist_ok=True) + + if with_code_image: + submitter_dir = tmp_path / division / orgname + submitter_dir.mkdir(parents=True, exist_ok=True) + _capture_code_image_at(submitter_dir) + + return leaf + + +def _capture_code_image_at(target_dir: Path): + """Capture a synthetic code image at target_dir/code/ (deterministic).""" + log = MagicMock() + src = target_dir / "_src" + src.mkdir(parents=True, exist_ok=True) + (src / "pyproject.toml").write_text("# stub\n", encoding="utf-8") + (src / "mod.py").write_text("# mod\n", encoding="utf-8") + capture_code_image(src, target_dir, log) + + +def _make_training_check( + leaf_path: Path, + division: str, + log, + *, + reference_checksum_override=None, +): + """Instantiate TrainingCheck against fake SubmissionLogs / LoaderMetadata.""" + config = Config( + version="v3.0", + submitters=None, + skip_output_file=True, + reference_checksum_override=reference_checksum_override, + ) + loader_metadata = LoaderMetadata( + division=division, + submitter="acme", + system="sys-1", + mode="training", + benchmark="unet3d", + folder=str(leaf_path), + ) + submissions_logs = SubmissionLogs( + datagen_files=[], + run_files=[], + system_file=None, + loader_metadata=loader_metadata, + ) + return TrainingCheck(log=log, config=config, submissions_logs=submissions_logs) + + +def _violations(mock_logger, rule_id: str, rule_name: str): + prefix = "[%s %s]" % (rule_id, rule_name) + return [m for m in mock_logger.errors if prefix in m] + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +def test_closed_training_self_consistent_passes(tmp_path, mock_logger): + """Self-consistent .code-hash.json under CLOSED → True, no violations.""" + leaf = _build_training_leaf( + tmp_path, "closed", "acme", "sys-1", with_code_image=True, + ) + check = _make_training_check(leaf, "closed", mock_logger) + assert check.closed_submission_checksum() is True + assert _violations(mock_logger, "3.6.1", "trainingClosedSubmissionChecksum") == [] + assert mock_logger.errors == [] + assert mock_logger.warnings == [] + + +def test_closed_training_self_consistency_violation(tmp_path, mock_logger): + """Tamper with .code-hash.json → violation tagged 3.6.1 (NOT 5.6.1 / 2.1.6). + + Load-bearing CD-04 wiring proof: the shared helper attributes the + violation to the caller's rule ID. If a regression hardcoded a rule + ID inside the helper, this test would fail because the violation + would carry the wrong tag. + """ + leaf = _build_training_leaf( + tmp_path, "closed", "acme", "sys-1", with_code_image=True, + ) + hash_file = tmp_path / "closed" / "acme" / "code" / ".code-hash.json" + payload = json.loads(hash_file.read_text(encoding="utf-8")) + payload["hash"] = "0" * 32 + hash_file.write_text(json.dumps(payload), encoding="utf-8") + + check = _make_training_check(leaf, "closed", mock_logger) + assert check.closed_submission_checksum() is False + viol = _violations(mock_logger, "3.6.1", "trainingClosedSubmissionChecksum") + assert len(viol) == 1, ( + "expected exactly one [3.6.1 trainingClosedSubmissionChecksum] violation; " + "found %s" % mock_logger.errors + ) + # Cross-rule guard: must NOT leak into 5.6.1 / 2.1.6 tags. + assert not _violations(mock_logger, "5.6.1", "vdbClosedSubmissionChecksum"), ( + "3.6.1 violation leaked into 5.6.1 vdbClosedSubmissionChecksum tag" + ) + assert not _violations(mock_logger, "2.1.6", "codeDirectoryContents"), ( + "3.6.1 violation leaked into 2.1.6 codeDirectoryContents tag" + ) + assert "code tree hash does not match" in viol[0] + + +def test_closed_training_upstream_identity_violation_when_reference_set( + tmp_path, mock_logger, +): + """REFERENCE_CHECKSUMS override that mismatches → violation tagged 3.6.1. + + Proves the upstream-identity branch of the layered helper is wired + through the caller's rule ID/name. + """ + leaf = _build_training_leaf( + tmp_path, "closed", "acme", "sys-1", with_code_image=True, + ) + bogus_ref = "ff" * 16 + check = _make_training_check( + leaf, "closed", mock_logger, + reference_checksum_override=bogus_ref, + ) + assert check.closed_submission_checksum() is False + viol = _violations(mock_logger, "3.6.1", "trainingClosedSubmissionChecksum") + assert len(viol) == 1, mock_logger.errors + assert "code tree MD5 mismatch" in viol[0] + assert bogus_ref in viol[0] + assert not _violations(mock_logger, "5.6.1", "vdbClosedSubmissionChecksum") + assert not _violations(mock_logger, "2.1.6", "codeDirectoryContents") + + +def test_open_training_is_noop(tmp_path, mock_logger): + """OPEN division → §3.6.1 short-circuits to True; STRUCT-06 self-consistency loop owns OPEN.""" + leaf = _build_training_leaf( + tmp_path, "open", "acme", "sys-1", with_code_image=True, + ) + check = _make_training_check(leaf, "open", mock_logger) + assert check.closed_submission_checksum() is True + assert mock_logger.errors == [] + assert mock_logger.warnings == [] + + +def test_missing_code_dir_does_not_double_violate(tmp_path, mock_logger): + """CLOSED with no code/ subdir → §3.6.1 no-ops; §2.1.6 owns VALS-01. + + Guards the design choice from Plan 04-02 Task 1 Step B item 4: + the missing-code/ structural violation is owned by STRUCT-06 (2.1.6), + and §3.6.1 must NOT double-count by re-firing. + """ + leaf = _build_training_leaf(tmp_path, "closed", "acme", "sys-1") + # No with_code_image=True → submitter dir has no code/. + check = _make_training_check(leaf, "closed", mock_logger) + assert check.closed_submission_checksum() is True + assert _violations(mock_logger, "3.6.1", "trainingClosedSubmissionChecksum") == [] + assert mock_logger.errors == [] diff --git a/mlpstorage_py/tests/test_vdb_checks.py b/mlpstorage_py/tests/test_vdb_checks.py new file mode 100644 index 00000000..3ad89944 --- /dev/null +++ b/mlpstorage_py/tests/test_vdb_checks.py @@ -0,0 +1,1028 @@ +"""Tests for Rules.md §5 — VdbCheck per-rule sweep (Phase 04 Plan 04-04). + +Exercises every ``@rule``-decorated method on ``VdbCheck`` (Phase 04 Plan 04-02) +through direct instantiation of ``VdbCheck`` against synthesised +``SubmissionLogs`` / ``LoaderMetadata`` fakes plus an on-disk +``vector_database//`` tree under ``tmp_path`` (Phase 04 Plan 04-01 +shape). One ``Test__`` class per §5.1.1–5.6.5 rule, each +with at least one happy-path case and one targeted-failure case. The 5.6.1 +class additionally proves the rule-id wiring through +``helpers._check_code_image_layered`` (D-06 / CD-04 at the test level). +""" + +import json +import os +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from mlpstorage_py.submission_checker.checks.vdb_checks import VdbCheck +from mlpstorage_py.submission_checker.configuration.configuration import Config +from mlpstorage_py.submission_checker.loader import LoaderMetadata, SubmissionLogs +from mlpstorage_py.submission_checker.tools.code_image import ( + capture_code_image, + find_source_root, +) + + +# --------------------------------------------------------------------------- +# Fixture builders +# --------------------------------------------------------------------------- + +_DEFAULT_RUN_TIMESTAMPS = [ + "20260618_120100", + "20260618_120200", + "20260618_120300", + "20260618_120400", + "20260618_120500", +] +_DEFAULT_DATAGEN_TIMESTAMPS = ["20260618_120000"] + + +def _build_vdb_leaf( + tmp_path: Path, + division: str, + orgname: str, + system: str, + index_type: str, + *, + run_timestamps=None, + datagen_timestamps=None, + with_code_image: bool = False, +) -> Path: + """Synthesize a vector_database submission tree under tmp_path. + + Shape: + ///results//vector_database// + [code/.code-hash.json + payload when with_code_image] + datagen// (one entry per datagen_timestamps) + run// (one entry per run_timestamps) + + No summary.json / metadata.json files are written here — the rule + methods read from the in-memory tuples populated on SubmissionLogs. + The disk tree only exists so the path-based rules (5.3.1 run count, + 5.6.3 dir-name match) see something real. ``index_type`` is the + UPPERCASE token (e.g. ``"DISKANN"``). Returns the per-leaf path + (``.../vector_database/``). + """ + if run_timestamps is None: + run_timestamps = _DEFAULT_RUN_TIMESTAMPS + if datagen_timestamps is None: + datagen_timestamps = _DEFAULT_DATAGEN_TIMESTAMPS + + leaf = ( + tmp_path + / division + / orgname + / "results" + / system + / "vector_database" + / index_type + ) + (leaf / "datagen").mkdir(parents=True, exist_ok=True) + (leaf / "run").mkdir(parents=True, exist_ok=True) + + for ts in datagen_timestamps: + (leaf / "datagen" / ts).mkdir(parents=True, exist_ok=True) + for ts in run_timestamps: + (leaf / "run" / ts).mkdir(parents=True, exist_ok=True) + + if with_code_image: + # Capture a code image at //code via the real + # capture helper so .code-hash.json is internally consistent. + submitter_dir = tmp_path / division / orgname + submitter_dir.mkdir(parents=True, exist_ok=True) + _capture_code_image_at(submitter_dir) + + return leaf + + +def _capture_code_image_at(target_dir: Path): + """Use the real capture helper to drop a valid code/ + .code-hash.json. + + Source is a small synthetic tree under target_dir/_src/ so each fixture + invocation produces a deterministic digest independent of the live + mlpstorage source tree. + """ + log = MagicMock() + src = target_dir / "_src" + src.mkdir(parents=True, exist_ok=True) + (src / "pyproject.toml").write_text("# stub\n", encoding="utf-8") + (src / "mod.py").write_text("# mod\n", encoding="utf-8") + capture_code_image(src, target_dir, log) + + +def _summary_run(**overrides): + """Build a §5-conformant run summary.json dict. + + Defaults satisfy every per-rule presence check; pass kwargs to + poke holes for targeted-failure cases. + """ + base = { + "num_vectors": 1_000_000, + "dimension": 128, + "index_type": "DISKANN", + "recall": 0.95, + "throughput_qps": 1000.0, + "total_time_seconds": 60.0, + "query_count": 60_000, + "mean_latency_ms": 1.0, + "p95_latency_ms": 2.0, + "p99_latency_ms": 3.0, + "p999_latency_ms": 4.0, + "database": {"database": "milvus"}, + } + base.update(overrides) + return base + + +def _summary_datagen(**overrides): + """Build a §5-conformant datagen summary.json dict.""" + base = { + "num_vectors": 1_000_000, + "dimension": 128, + "index_type": "DISKANN", + "inserted_vectors": 1_000_000, + } + base.update(overrides) + return base + + +def _metadata(**arg_overrides): + """Build a metadata.json dict with args + params_dict. + + Pop "params_dict" to override the params dict itself; everything else + is treated as an args.* override. + """ + params_dict = arg_overrides.pop("params_dict", None) + args = { + "storage_root": "/vdb/data", + "results_dir": "/vdb/results", + } + args.update(arg_overrides) + return { + "args": args, + "params_dict": params_dict if params_dict is not None else {}, + } + + +def _make_vdb_check( + leaf_path: Path, + division: str, + log, + *, + run_files=None, + datagen_files=None, + system_file=None, + mode: str = "vector_database", + reference_checksum_override=None, +): + """Instantiate VdbCheck against fake SubmissionLogs / LoaderMetadata.""" + config = Config( + version="v3.0", + submitters=None, + skip_output_file=True, + reference_checksum_override=reference_checksum_override, + ) + loader_metadata = LoaderMetadata( + division=division, + submitter="acme", + system="sys-1", + mode=mode, + benchmark=os.path.basename(str(leaf_path).rstrip(os.sep)), + folder=str(leaf_path), + ) + submissions_logs = SubmissionLogs( + datagen_files=datagen_files or [], + run_files=run_files or [], + system_file=system_file, + loader_metadata=loader_metadata, + ) + return VdbCheck(log=log, config=config, submissions_logs=submissions_logs) + + +def _violations(mock_logger, rule_id: str, rule_name: str): + """Return mock_logger.errors entries tagged with the given rule prefix.""" + prefix = "[%s %s]" % (rule_id, rule_name) + return [m for m in mock_logger.errors if prefix in m] + + +def _warnings(mock_logger, rule_id: str, rule_name: str): + """Return mock_logger.warnings entries tagged with the given rule prefix.""" + prefix = "[%s %s]" % (rule_id, rule_name) + return [m for m in mock_logger.warnings if prefix in m] + + +# =========================================================================== +# Mode-guard sweep — proves all 16 rules no-op on non-vdb submissions +# =========================================================================== + +class TestModeGuardNoOpsOnNonVdbSubmissions: + """All 16 §5 rule methods must no-op when mode != "vector_database". + + Proves the post-Plan-04-01 guard string is "vector_database" (not + "vector_database"). A regression to the old guard string would + cause every method to no-op on real vdb submissions too. + """ + + def test_all_rules_noop_on_training_mode(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + check = _make_vdb_check( + leaf, "closed", mock_logger, + run_files=[], + datagen_files=[], + mode="training", + ) + rule_methods = [ + "vdb_dataset_scale", "vdb_dimension_consistency", + "vdb_collection_populated", "vdb_index_build_completed", + "vdb_run_count", "vdb_recall_reported", + "vdb_query_count_minimum", "vdb_metrics_reported", + "vdb_path_args", "vdb_filesystem_check", + "vdb_object_storage_backend", "vdb_closed_submission_checksum", + "vdb_closed_database_backend", "vdb_closed_index_types", + "vdb_closed_submission_parameters", + "vdb_open_submission_parameters", + ] + for name in rule_methods: + assert getattr(check, name)() is True, ( + f"{name} returned non-True under mode=training" + ) + assert mock_logger.errors == [], mock_logger.errors + assert mock_logger.warnings == [], mock_logger.warnings + + +# =========================================================================== +# §5.1.1 vdbDatasetScale +# =========================================================================== + +class Test_5_1_1_VdbDatasetScale: + """§5.1.1 — Per-run scale (num_vectors, dimension) presence check. + + Plan 04-02 noted the scale table is deferred — a warn_violation + is emitted unconditionally per leaf; the rule still fails when + num_vectors / dimension are absent from a run summary. + """ + + def test_happy_path_present_fields_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [(_summary_run(), _metadata(), ts) for ts in _DEFAULT_RUN_TIMESTAMPS] + check = _make_vdb_check( + leaf, "closed", mock_logger, + run_files=run_files, + ) + assert check.vdb_dataset_scale() is True + # The deferred-data warning is expected. + assert _warnings(mock_logger, "5.1.1", "vdbDatasetScale"), ( + "expected deferred scale-table warn" + ) + assert _violations(mock_logger, "5.1.1", "vdbDatasetScale") == [] + + def test_missing_num_vectors_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + bad_summary = _summary_run() + bad_summary.pop("num_vectors") + run_files = [(bad_summary, _metadata(), "20260618_120100")] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_dataset_scale() is False + viol = _violations(mock_logger, "5.1.1", "vdbDatasetScale") + assert any("missing num_vectors" in v for v in viol), viol + + +# =========================================================================== +# §5.1.2 vdbDimensionConsistency +# =========================================================================== + +class Test_5_1_2_VdbDimensionConsistency: + + def test_matching_dimensions_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + datagen_files = [(_summary_datagen(dimension=128), _metadata(), "20260618_120000")] + run_files = [(_summary_run(dimension=128), _metadata(), "20260618_120100")] + check = _make_vdb_check( + leaf, "closed", mock_logger, + datagen_files=datagen_files, run_files=run_files, + ) + assert check.vdb_dimension_consistency() is True + assert _violations(mock_logger, "5.1.2", "vdbDimensionConsistency") == [] + + def test_dimension_mismatch_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + datagen_files = [(_summary_datagen(dimension=128), _metadata(), "20260618_120000")] + run_files = [(_summary_run(dimension=256), _metadata(), "20260618_120100")] + check = _make_vdb_check( + leaf, "closed", mock_logger, + datagen_files=datagen_files, run_files=run_files, + ) + assert check.vdb_dimension_consistency() is False + viol = _violations(mock_logger, "5.1.2", "vdbDimensionConsistency") + assert any("dimension mismatch" in v for v in viol), viol + assert any("128" in v and "256" in v for v in viol), viol + + +# =========================================================================== +# §5.2.1 vdbCollectionPopulated +# =========================================================================== + +class Test_5_2_1_VdbCollectionPopulated: + + def test_inserted_equals_declared_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + datagen_files = [ + (_summary_datagen(num_vectors=1_000_000, inserted_vectors=1_000_000), + _metadata(), "20260618_120000"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, datagen_files=datagen_files, + ) + assert check.vdb_collection_populated() is True + assert _violations(mock_logger, "5.2.1", "vdbCollectionPopulated") == [] + + def test_underpopulated_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + datagen_files = [ + (_summary_datagen(num_vectors=1_000_000, inserted_vectors=999_999), + _metadata(), "20260618_120000"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, datagen_files=datagen_files, + ) + assert check.vdb_collection_populated() is False + viol = _violations(mock_logger, "5.2.1", "vdbCollectionPopulated") + assert any("underpopulated" in v for v in viol), viol + + +# =========================================================================== +# §5.2.2 vdbIndexBuildCompleted +# =========================================================================== + +class Test_5_2_2_VdbIndexBuildCompleted: + + def test_matching_index_types_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + datagen_files = [(_summary_datagen(index_type="DISKANN"), _metadata(), "20260618_120000")] + run_files = [(_summary_run(index_type="DISKANN"), _metadata(), "20260618_120100")] + check = _make_vdb_check( + leaf, "closed", mock_logger, + datagen_files=datagen_files, run_files=run_files, + ) + assert check.vdb_index_build_completed() is True + assert _violations(mock_logger, "5.2.2", "vdbIndexBuildCompleted") == [] + + def test_index_type_drift_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + datagen_files = [(_summary_datagen(index_type="DISKANN"), _metadata(), "20260618_120000")] + run_files = [(_summary_run(index_type="HNSW"), _metadata(), "20260618_120100")] + check = _make_vdb_check( + leaf, "closed", mock_logger, + datagen_files=datagen_files, run_files=run_files, + ) + assert check.vdb_index_build_completed() is False + viol = _violations(mock_logger, "5.2.2", "vdbIndexBuildCompleted") + assert any("index_type changed" in v for v in viol), viol + + def test_missing_index_type_at_datagen_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + bad_datagen = _summary_datagen() + bad_datagen.pop("index_type") + datagen_files = [(bad_datagen, _metadata(), "20260618_120000")] + check = _make_vdb_check( + leaf, "closed", mock_logger, datagen_files=datagen_files, + ) + assert check.vdb_index_build_completed() is False + viol = _violations(mock_logger, "5.2.2", "vdbIndexBuildCompleted") + assert any("missing index_type" in v for v in viol), viol + + +# =========================================================================== +# §5.3.1 vdbRunCount +# =========================================================================== + +class Test_5_3_1_VdbRunCount: + """§5.3.1 walks the on-disk run/ dir, not the loader's run_files. + + Phase 4 D-04: the count of exactly five applies to run/, not datagen/. + """ + + def test_exactly_five_run_timestamps_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + run_timestamps=_DEFAULT_RUN_TIMESTAMPS, + ) + check = _make_vdb_check(leaf, "closed", mock_logger) + assert check.vdb_run_count() is True + assert _violations(mock_logger, "5.3.1", "vdbRunCount") == [] + + def test_three_run_timestamps_log_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + run_timestamps=["20260618_120100", "20260618_120200", "20260618_120300"], + ) + check = _make_vdb_check(leaf, "closed", mock_logger) + assert check.vdb_run_count() is False + viol = _violations(mock_logger, "5.3.1", "vdbRunCount") + assert any("expected exactly 5" in v and "found 3" in v for v in viol), viol + + +# =========================================================================== +# §5.3.2 vdbRecallReported +# =========================================================================== + +class Test_5_3_2_VdbRecallReported: + """The minimum-recall target table is deferred (warn_violation per leaf).""" + + def test_recall_present_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [(_summary_run(recall=0.95), _metadata(), ts) for ts in _DEFAULT_RUN_TIMESTAMPS] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_recall_reported() is True + assert _warnings(mock_logger, "5.3.2", "vdbRecallReported"), ( + "expected deferred recall-table warn" + ) + assert _violations(mock_logger, "5.3.2", "vdbRecallReported") == [] + + def test_missing_recall_without_fallback_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + bad_summary = _summary_run() + bad_summary.pop("recall") + # No recall_stats.json fallback file present. + run_files = [(bad_summary, _metadata(), "20260618_120100")] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_recall_reported() is False + viol = _violations(mock_logger, "5.3.2", "vdbRecallReported") + assert any("no recall value" in v for v in viol), viol + + +# =========================================================================== +# §5.3.3 vdbQueryCountMinimum +# =========================================================================== + +class Test_5_3_3_VdbQueryCountMinimum: + """The minimum-query target table is deferred (warn_violation per leaf).""" + + def test_qps_and_total_time_present_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [(_summary_run(), _metadata(), ts) for ts in _DEFAULT_RUN_TIMESTAMPS] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_query_count_minimum() is True + assert _warnings(mock_logger, "5.3.3", "vdbQueryCountMinimum"), ( + "expected deferred query-table warn" + ) + assert _violations(mock_logger, "5.3.3", "vdbQueryCountMinimum") == [] + + def test_missing_qps_and_query_count_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + bad_summary = _summary_run() + bad_summary.pop("throughput_qps") + bad_summary.pop("query_count") + run_files = [(bad_summary, _metadata(), "20260618_120100")] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_query_count_minimum() is False + viol = _violations(mock_logger, "5.3.3", "vdbQueryCountMinimum") + assert any("cannot compute issued queries" in v for v in viol), viol + + +# =========================================================================== +# §5.3.4 vdbMetricsReported +# =========================================================================== + +class Test_5_3_4_VdbMetricsReported: + + def test_all_required_fields_present_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [(_summary_run(), _metadata(), ts) for ts in _DEFAULT_RUN_TIMESTAMPS] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_metrics_reported() is True + assert _violations(mock_logger, "5.3.4", "vdbMetricsReported") == [] + + def test_missing_p999_latency_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + bad_summary = _summary_run() + bad_summary.pop("p999_latency_ms") + run_files = [(bad_summary, _metadata(), "20260618_120100")] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_metrics_reported() is False + viol = _violations(mock_logger, "5.3.4", "vdbMetricsReported") + assert any("'p999_latency_ms' missing" in v for v in viol), viol + + +# =========================================================================== +# §5.4.1 vdbPathArgs +# =========================================================================== + +class Test_5_4_1_VdbPathArgs: + + def test_distinct_paths_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [ + (_summary_run(), + _metadata(storage_root="/vdb/data", results_dir="/vdb/results"), + "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_path_args() is True + assert _violations(mock_logger, "5.4.1", "vdbPathArgs") == [] + + def test_equal_paths_log_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [ + (_summary_run(), + _metadata(storage_root="/shared", results_dir="/shared"), + "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_path_args() is False + viol = _violations(mock_logger, "5.4.1", "vdbPathArgs") + assert any("must differ" in v for v in viol), viol + + +# =========================================================================== +# §5.4.2 vdbFilesystemCheck +# =========================================================================== + +class Test_5_4_2_VdbFilesystemCheck: + """Reuses _check_filesystem_separation; reads df output from a logfile.""" + + _DF_DIFFERENT_MOUNTS = ( + "Filesystem 1K-blocks Used Available Use% Mounted on\n" + "/dev/sda1 1000 500 500 50% /vdb/data\n" + "/dev/sda2 1000 500 500 50% /vdb/results\n" + ) + _DF_SAME_MOUNT = ( + "Filesystem 1K-blocks Used Available Use% Mounted on\n" + "/dev/sda1 1000 500 500 50% /shared\n" + ) + + def test_different_filesystems_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + ts = "20260618_120100" + (leaf / "run" / ts / "vdb_run.stdout.log").write_text( + self._DF_DIFFERENT_MOUNTS, encoding="utf-8", + ) + run_files = [ + (_summary_run(), + _metadata(storage_root="/vdb/data", results_dir="/vdb/results"), + ts), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_filesystem_check() is True + assert _violations(mock_logger, "5.4.2", "vdbFilesystemCheck") == [] + + def test_same_filesystem_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + ts = "20260618_120100" + (leaf / "run" / ts / "vdb_run.stdout.log").write_text( + self._DF_SAME_MOUNT, encoding="utf-8", + ) + run_files = [ + (_summary_run(), + _metadata(storage_root="/shared", results_dir="/shared"), + ts), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_filesystem_check() is False + viol = _violations(mock_logger, "5.4.2", "vdbFilesystemCheck") + assert any("same filesystem" in v for v in viol), viol + + +# =========================================================================== +# §5.5.1 vdbObjectStorageBackend +# =========================================================================== + +class Test_5_5_1_VdbObjectStorageBackend: + + def _object_system_file(self): + return { + "system_under_test": { + "solution": { + "architecture": {"benchmark_API": "object"}, + }, + }, + } + + def _file_system_file(self): + return { + "system_under_test": { + "solution": { + "architecture": {"benchmark_API": "file"}, + }, + }, + } + + def test_object_api_with_s3_backend_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [ + (_summary_run(database={"database": "milvus", "storage_backend": "s3"}), + _metadata(), "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, + run_files=run_files, + system_file=self._object_system_file(), + ) + assert check.vdb_object_storage_backend() is True + assert _violations(mock_logger, "5.5.1", "vdbObjectStorageBackend") == [] + + def test_object_api_with_non_s3_backend_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [ + (_summary_run(database={"database": "milvus", "storage_backend": "nfs"}), + _metadata(), "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, + run_files=run_files, + system_file=self._object_system_file(), + ) + assert check.vdb_object_storage_backend() is False + viol = _violations(mock_logger, "5.5.1", "vdbObjectStorageBackend") + assert any("S3-compatible" in v for v in viol), viol + + def test_file_api_is_noop_regardless_of_backend(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + # Non-s3 backend but file API → must no-op. + run_files = [ + (_summary_run(database={"database": "milvus", "storage_backend": "nfs"}), + _metadata(), "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, + run_files=run_files, + system_file=self._file_system_file(), + ) + assert check.vdb_object_storage_backend() is True + assert _violations(mock_logger, "5.5.1", "vdbObjectStorageBackend") == [] + + +# =========================================================================== +# §5.6.1 vdbClosedSubmissionChecksum — load-bearing CD-04 / D-06 wiring tests +# =========================================================================== + +class Test_5_6_1_VdbClosedSubmissionChecksum: + """Exercise helpers._check_code_image_layered via VdbCheck's rule ID. + + These tests are the load-bearing wiring proofs that violation messages + are tagged with 5.6.1 / vdbClosedSubmissionChecksum (NOT 2.1.6 / + codeDirectoryContents) when the helper is invoked through VdbCheck. + """ + + def test_closed_self_consistent_passes(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + with_code_image=True, + ) + check = _make_vdb_check(leaf, "closed", mock_logger) + assert check.vdb_closed_submission_checksum() is True + assert _violations(mock_logger, "5.6.1", "vdbClosedSubmissionChecksum") == [] + assert _warnings(mock_logger, "5.6.1", "vdbClosedSubmissionChecksum") == [] + + def test_closed_self_consistency_violation_uses_5_6_1_rule_id( + self, tmp_path, mock_logger, + ): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + with_code_image=True, + ) + # Tamper with .code-hash.json to break self-consistency. + hash_file = tmp_path / "closed" / "acme" / "code" / ".code-hash.json" + payload = json.loads(hash_file.read_text(encoding="utf-8")) + payload["hash"] = "0" * 32 + hash_file.write_text(json.dumps(payload), encoding="utf-8") + + check = _make_vdb_check(leaf, "closed", mock_logger) + assert check.vdb_closed_submission_checksum() is False + # Exactly one 5.6.1 violation — and it MUST NOT be a 2.1.6 violation. + viol = _violations(mock_logger, "5.6.1", "vdbClosedSubmissionChecksum") + assert len(viol) == 1, ( + "expected exactly one [5.6.1 vdbClosedSubmissionChecksum] violation; " + "found %s" % mock_logger.errors + ) + # Rule-id-wiring guard: must NOT misreport as 2.1.6. + assert not _violations(mock_logger, "2.1.6", "codeDirectoryContents"), ( + "5.6.1 violation leaked into 2.1.6 codeDirectoryContents tag" + ) + assert "code tree hash does not match" in viol[0] + + def test_closed_upstream_identity_violation_when_reference_set( + self, tmp_path, mock_logger, + ): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + with_code_image=True, + ) + # Configure a reference checksum that will NOT match. + bogus_ref = "ff" * 16 + check = _make_vdb_check( + leaf, "closed", mock_logger, + reference_checksum_override=bogus_ref, + ) + assert check.vdb_closed_submission_checksum() is False + viol = _violations(mock_logger, "5.6.1", "vdbClosedSubmissionChecksum") + assert len(viol) == 1, mock_logger.errors + assert "code tree MD5 mismatch" in viol[0] + assert bogus_ref in viol[0] + assert not _violations(mock_logger, "2.1.6", "codeDirectoryContents"), ( + "5.6.1 upstream-identity violation misreported as 2.1.6" + ) + + def test_open_division_is_noop(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "open", "acme", "sys-1", "DISKANN", + with_code_image=True, + ) + check = _make_vdb_check(leaf, "open", mock_logger) + assert check.vdb_closed_submission_checksum() is True + assert mock_logger.errors == [] + assert mock_logger.warnings == [] + + def test_missing_code_dir_does_not_double_violate(self, tmp_path, mock_logger): + # CLOSED but no code/ — STRUCT-06 owns the missing-code violation. + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + check = _make_vdb_check(leaf, "closed", mock_logger) + assert check.vdb_closed_submission_checksum() is True + assert _violations(mock_logger, "5.6.1", "vdbClosedSubmissionChecksum") == [] + + +# =========================================================================== +# §5.6.2 vdbClosedDatabaseBackend +# =========================================================================== + +class Test_5_6_2_VdbClosedDatabaseBackend: + + def test_closed_milvus_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [ + (_summary_run(database={"database": "milvus"}), + _metadata(), "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_closed_database_backend() is True + assert _violations(mock_logger, "5.6.2", "vdbClosedDatabaseBackend") == [] + + def test_closed_elasticsearch_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [ + (_summary_run(database={"database": "elasticsearch"}), + _metadata(), "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_closed_database_backend() is False + viol = _violations(mock_logger, "5.6.2", "vdbClosedDatabaseBackend") + assert any("CLOSED requires milvus backend" in v for v in viol), viol + + +# =========================================================================== +# §5.6.3 vdbClosedIndexTypes — D-03 dual-vocabulary at the test level +# =========================================================================== + +class Test_5_6_3_VdbClosedIndexTypes: + """Dir-name vs summary.index_type comparison: both are UPPERCASE tokens.""" + + def test_closed_diskann_dir_with_diskann_index_type_passes( + self, tmp_path, mock_logger, + ): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [ + (_summary_run(index_type="DISKANN"), _metadata(), "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_closed_index_types() is True + assert _violations(mock_logger, "5.6.3", "vdbClosedIndexTypes") == [] + + def test_closed_aisaq_passes(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "AISAQ", + ) + run_files = [ + (_summary_run(index_type="AISAQ"), _metadata(), "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_closed_index_types() is True + assert _violations(mock_logger, "5.6.3", "vdbClosedIndexTypes") == [] + + def test_closed_unknown_dir_name_violation(self, tmp_path, mock_logger): + # IVF_FLAT is in the OPEN-extended set but NOT in + # VDB_INDEX_TYPES_CLOSED — CLOSED disallows it. + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "IVF_FLAT", + ) + run_files = [ + (_summary_run(index_type="IVF_FLAT"), _metadata(), "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_closed_index_types() is False + viol = _violations(mock_logger, "5.6.3", "vdbClosedIndexTypes") + assert any("not a CLOSED index" in v for v in viol), viol + + def test_closed_dir_index_type_mismatch_violation(self, tmp_path, mock_logger): + # On-disk says DISKANN but summary.json says HNSW. + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + run_files = [ + (_summary_run(index_type="HNSW"), _metadata(), "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_closed_index_types() is False + viol = _violations(mock_logger, "5.6.3", "vdbClosedIndexTypes") + assert any("DISKANN" in v and "HNSW" in v for v in viol), viol + + +# =========================================================================== +# §5.6.4 vdbClosedSubmissionParameters +# =========================================================================== + +class Test_5_6_4_VdbClosedSubmissionParameters: + + def test_only_allowed_params_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + # All keys below are in the CLOSED allowlist (vdb_checks.py). + params = { + "index.index_type": "DISKANN", + "index.metric_type": "L2", + "run.batch_size": 100, + } + run_files = [ + (_summary_run(), + _metadata(params_dict=params), + "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_closed_submission_parameters() is True + assert _violations(mock_logger, "5.6.4", "vdbClosedSubmissionParameters") == [] + + def test_disallowed_param_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "closed", "acme", "sys-1", "DISKANN", + ) + # database.host is OPEN-only; CLOSED must reject it. + params = {"database.host": "10.0.0.1"} + run_files = [ + (_summary_run(), + _metadata(params_dict=params), + "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "closed", mock_logger, run_files=run_files, + ) + assert check.vdb_closed_submission_parameters() is False + viol = _violations(mock_logger, "5.6.4", "vdbClosedSubmissionParameters") + assert any("database.host" in v for v in viol), viol + + +# =========================================================================== +# §5.6.5 vdbOpenSubmissionParameters +# =========================================================================== + +class Test_5_6_5_VdbOpenSubmissionParameters: + + def test_open_milvus_with_open_extras_pass(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "open", "acme", "sys-1", "DISKANN", + ) + # CLOSED set + OPEN extras (database.host, database.port). + params = { + "index.index_type": "DISKANN", + "database.host": "10.0.0.1", + "database.port": 19530, + } + run_files = [ + (_summary_run(database={"database": "milvus"}), + _metadata(params_dict=params), + "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "open", mock_logger, run_files=run_files, + ) + assert check.vdb_open_submission_parameters() is True + assert _violations(mock_logger, "5.6.5", "vdbOpenSubmissionParameters") == [] + + def test_open_milvus_disallowed_param_logs_violation(self, tmp_path, mock_logger): + leaf = _build_vdb_leaf( + tmp_path, "open", "acme", "sys-1", "DISKANN", + ) + # Milvus backend with a param outside the OPEN allowlist. + params = {"index.unknown_param": "x"} + run_files = [ + (_summary_run(database={"database": "milvus"}), + _metadata(params_dict=params), + "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "open", mock_logger, run_files=run_files, + ) + assert check.vdb_open_submission_parameters() is False + viol = _violations(mock_logger, "5.6.5", "vdbOpenSubmissionParameters") + assert any("index.unknown_param" in v for v in viol), viol + + def test_open_non_milvus_backend_warns_and_relaxes(self, tmp_path, mock_logger): + # OPEN with elasticsearch: relax strict allowlist; warn instead. + leaf = _build_vdb_leaf( + tmp_path, "open", "acme", "sys-1", "DISKANN", + ) + params = {"index.elastic_native_param": "x"} + run_files = [ + (_summary_run(database={"database": "elasticsearch"}), + _metadata(params_dict=params), + "20260618_120100"), + ] + check = _make_vdb_check( + leaf, "open", mock_logger, run_files=run_files, + ) + assert check.vdb_open_submission_parameters() is True + assert _violations(mock_logger, "5.6.5", "vdbOpenSubmissionParameters") == [] + warns = _warnings(mock_logger, "5.6.5", "vdbOpenSubmissionParameters") + assert any("non-Milvus backend" in w for w in warns), warns diff --git a/pyproject.toml b/pyproject.toml index ae5b90cf..d5bffc1c 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mlpstorage" -version = "3.0.15" +version = "3.0.16" description = "MLPerf Storage Benchmark Suite" readme = "README.md" license = {text = "Apache-2.0"} diff --git a/tests/unit/test_accumulation.py b/tests/unit/test_accumulation.py index ed3c5061..ce0a945c 100644 --- a/tests/unit/test_accumulation.py +++ b/tests/unit/test_accumulation.py @@ -408,7 +408,11 @@ class TestPreviewBenchmarkAccumulation: as the distinguishing component in path and metadata.""" def test_vectordb_path_includes_engine_and_index(self, tmp_path): - """Path is vector_database////.""" + """Path is vector_database////. + + The index directory uses the UPPERCASE token (e.g. "DISKANN"), + matching args.vdb_index / args.index_type. + """ from types import SimpleNamespace from mlpstorage_py.config import BENCHMARK_TYPES as _BT diff --git a/tests/unit/test_benchmarks_base.py b/tests/unit/test_benchmarks_base.py index c0713711..fe9772ab 100755 --- a/tests/unit/test_benchmarks_base.py +++ b/tests/unit/test_benchmarks_base.py @@ -528,7 +528,9 @@ def test_calls_generate_output_location(self, tmp_path): result = benchmark.generate_output_location() - mock_gen.assert_called_once_with(benchmark, "20250115_120000") + mock_gen.assert_called_once_with( + benchmark, "20250115_120000", orgname=None, systemname=None + ) class TestBenchmarkIntegration: diff --git a/uv.lock b/uv.lock index dd8e4bb4..984d982d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 3 requires-python = "==3.12.*" resolution-markers = [ "sys_platform == 'linux'", @@ -11,25 +12,25 @@ supported-markers = [ name = "absl-py" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543 } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750 }, + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "antlr4-python3-runtime" version = "4.9.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034 } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } [[package]] name = "anyio" @@ -39,9 +40,9 @@ dependencies = [ { name = "idna", marker = "sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506 }, + { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, ] [[package]] @@ -51,9 +52,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argon2-cffi-bindings", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706 } +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657 }, + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, ] [[package]] @@ -63,12 +64,12 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246 }, - { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126 }, - { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343 }, - { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777 }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, ] [[package]] @@ -79,27 +80,27 @@ dependencies = [ { name = "six", marker = "sys_platform == 'linux'" }, { name = "wheel", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732 }, + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, ] [[package]] name = "cachetools" version = "7.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761 }, + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, ] [[package]] name = "certifi" version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289 }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -109,55 +110,55 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy' and sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061 }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031 }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239 }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589 }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733 }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652 }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229 }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552 }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806 }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316 }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274 }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468 }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958 }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "coverage" version = "7.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132 } +sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888 }, - { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624 }, - { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739 }, - { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998 }, - { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296 }, - { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658 }, - { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803 }, - { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873 }, - { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245 }, - { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815 }, + { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, + { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, ] [[package]] @@ -168,8 +169,8 @@ dependencies = [ { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351 }, - { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965 }, + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, ] [[package]] @@ -177,7 +178,7 @@ name = "cuda-pathfinder" version = "1.5.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671 }, + { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" }, ] [[package]] @@ -185,7 +186,7 @@ name = "cuda-toolkit" version = "13.0.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364 }, + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, ] [package.optional-dependencies] @@ -227,10 +228,10 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zstandard", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/f839357750c2229643abf2627b43d0f12d6984e79ba6891522a3aabc52b6/dgen_py-0.2.4.tar.gz", hash = "sha256:a1820092a1ac4a793ceda1db30de66339b7a75fd8e609f6cb6be84c31ecdb625", size = 217909 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/f839357750c2229643abf2627b43d0f12d6984e79ba6891522a3aabc52b6/dgen_py-0.2.4.tar.gz", hash = "sha256:a1820092a1ac4a793ceda1db30de66339b7a75fd8e609f6cb6be84c31ecdb625", size = 217909, upload-time = "2026-05-05T16:00:33.731Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/91/2dae75d696c0f9e380acc7bcda09ccddb70d27455dab59e0c90424fe5881/dgen_py-0.2.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e49af6efdbd11860f24ce804bd1a1b3b6b71a1f5f5de55b33977f14ad9bc41ab", size = 394488 }, - { url = "https://files.pythonhosted.org/packages/a9/54/2f7d900bee5be6177a3c7b25fe50699217c722efa0fc2f05a4366bb3cfec/dgen_py-0.2.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8acba9dfc8512e9dcfa1b4496d11b8511a35c7a4611290f769792a250e61a4f7", size = 404759 }, + { url = "https://files.pythonhosted.org/packages/2b/91/2dae75d696c0f9e380acc7bcda09ccddb70d27455dab59e0c90424fe5881/dgen_py-0.2.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e49af6efdbd11860f24ce804bd1a1b3b6b71a1f5f5de55b33977f14ad9bc41ab", size = 394488, upload-time = "2026-05-09T16:44:21.341Z" }, + { url = "https://files.pythonhosted.org/packages/a9/54/2f7d900bee5be6177a3c7b25fe50699217c722efa0fc2f05a4366bb3cfec/dgen_py-0.2.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8acba9dfc8512e9dcfa1b4496d11b8511a35c7a4611290f769792a250e61a4f7", size = 404759, upload-time = "2026-05-05T16:00:29.417Z" }, ] [[package]] @@ -266,9 +267,9 @@ dependencies = [ { name = "sniffio", marker = "sys_platform == 'linux'" }, { name = "urllib3", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/be/a719a217ed7f179c9e41821dff9dfd28415b21bc94da9d35fff84f7951b2/elastic_transport-9.4.2.tar.gz", hash = "sha256:366f4614f4544c5fb5d780c82f57af8f30492b44a68ed20750390aa81e20c2ea", size = 79310 } +sdist = { url = "https://files.pythonhosted.org/packages/52/be/a719a217ed7f179c9e41821dff9dfd28415b21bc94da9d35fff84f7951b2/elastic_transport-9.4.2.tar.gz", hash = "sha256:366f4614f4544c5fb5d780c82f57af8f30492b44a68ed20750390aa81e20c2ea", size = 79310, upload-time = "2026-06-16T15:27:17.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/e4/9b7ef7a8e0bb416e0fe1879c92295041cf81e5a47e3c3110744e89b6eb35/elastic_transport-9.4.2-py3-none-any.whl", hash = "sha256:33dc89bb1855faa8b98ae8b036405a39c562778dbcdbe4a00a2eaf753148556c", size = 66314 }, + { url = "https://files.pythonhosted.org/packages/0b/e4/9b7ef7a8e0bb416e0fe1879c92295041cf81e5a47e3c3110744e89b6eb35/elastic_transport-9.4.2-py3-none-any.whl", hash = "sha256:33dc89bb1855faa8b98ae8b036405a39c562778dbcdbe4a00a2eaf753148556c", size = 66314, upload-time = "2026-06-16T15:27:15.875Z" }, ] [[package]] @@ -282,18 +283,18 @@ dependencies = [ { name = "sniffio", marker = "sys_platform == 'linux'" }, { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/4b/9b753f0a8f56ae508dced2f7ac87bef7a27ce8f890e349e16812e9f7f4fa/elasticsearch-9.4.1.tar.gz", hash = "sha256:1d78fdfba97a903ec35a5eb5808a74e33392b7c620bd5f742d465a3a26c27d75", size = 908138 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4b/9b753f0a8f56ae508dced2f7ac87bef7a27ce8f890e349e16812e9f7f4fa/elasticsearch-9.4.1.tar.gz", hash = "sha256:1d78fdfba97a903ec35a5eb5808a74e33392b7c620bd5f742d465a3a26c27d75", size = 908138, upload-time = "2026-05-26T16:28:40.132Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/8e/2c93805e93e724a90156004a9212572ec86473974deede4605a33b8b169a/elasticsearch-9.4.1-py3-none-any.whl", hash = "sha256:71ab71c3d1b20fd88c2922fb82c3277cce7ea03c160686e7b9368b265c2b4cac", size = 993647 }, + { url = "https://files.pythonhosted.org/packages/cf/8e/2c93805e93e724a90156004a9212572ec86473974deede4605a33b8b169a/elasticsearch-9.4.1-py3-none-any.whl", hash = "sha256:71ab71c3d1b20fd88c2922fb82c3277cce7ea03c160686e7b9368b265c2b4cac", size = 993647, upload-time = "2026-05-26T16:28:36.556Z" }, ] [[package]] name = "filelock" version = "3.29.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028 } +sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757 }, + { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, ] [[package]] @@ -301,25 +302,25 @@ name = "flatbuffers" version = "25.12.19" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661 }, + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] [[package]] name = "fsspec" version = "2026.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646 } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949 }, + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, ] [[package]] name = "gast" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/f6/e73969782a2ecec280f8a176f2476149dd9dba69d5f8779ec6108a7721e6/gast-0.7.0.tar.gz", hash = "sha256:0bb14cd1b806722e91ddbab6fb86bba148c22b40e7ff11e248974e04c8adfdae", size = 33630 } +sdist = { url = "https://files.pythonhosted.org/packages/91/f6/e73969782a2ecec280f8a176f2476149dd9dba69d5f8779ec6108a7721e6/gast-0.7.0.tar.gz", hash = "sha256:0bb14cd1b806722e91ddbab6fb86bba148c22b40e7ff11e248974e04c8adfdae", size = 33630, upload-time = "2025-11-29T15:30:05.266Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966 }, + { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966, upload-time = "2025-11-29T15:30:03.983Z" }, ] [[package]] @@ -329,9 +330,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430 } +sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430, upload-time = "2020-03-13T18:57:50.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471 }, + { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471, upload-time = "2020-03-13T18:57:48.872Z" }, ] [[package]] @@ -341,15 +342,15 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/07/9a979c81738863a738dc23d65177056e71fbb2db817740ed870b33434e7a/grpcio-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8b39472beafc0bdcafc4c8c73ad082ebfdb449d566897a61e7acb4fa88089115", size = 6053264 }, - { url = "https://files.pythonhosted.org/packages/e0/44/f257b7e0bd69c93b06c6cb8ac8d1b901ccb42bedabd83c1a4c77a71f8810/grpcio-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1e123f9b37edb8375fd74130d1f69c944bbf0a7b06761ae7211154b8759e94d2", size = 6595983 }, - { url = "https://files.pythonhosted.org/packages/b9/f3/19782aa04c960968bef8c5539329d8e3bbc3364e2e46d19eb5e5cc5e43b7/grpcio-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2c2e2ae6867c2966b8daccc836d54a13218e0007e9a490aeb81dd05be64d22d7", size = 7303455 }, - { url = "https://files.pythonhosted.org/packages/eb/8c/dea020b6d91508cd84463917a63149ec196ee7db505d032ae43fcb3303b9/grpcio-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:766bc7c9a9c340342f4c864ccbda8e78111e4751f13b895812b9c148fb79e9d0", size = 6809167 }, - { url = "https://files.pythonhosted.org/packages/1c/c7/3030dd940408083bd32cd95d634777a71605ade4887154d93e8a89244946/grpcio-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b259a04a737cb3496be0901328eb8b7552ed8df4865d8c8f1cf1bffcfc0776a3", size = 7412536 }, - { url = "https://files.pythonhosted.org/packages/e0/dd/1172a9e42b168edcafefad6115346ef619a3fc02158bb170e66ced24bcdd/grpcio-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:85b10a45b8993d195c4f3ff57025b8d1e11834909ee475c403bfa60cb4caefaf", size = 8408276 }, - { url = "https://files.pythonhosted.org/packages/25/7a/71437c7f3596e5246155c515852795a85a1a8d228190212432b13b97a95d/grpcio-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8ea1936c26b99999b27479853039a7f34713f56c49375ad52b38535ec93a796c", size = 7849660 }, + { url = "https://files.pythonhosted.org/packages/85/07/9a979c81738863a738dc23d65177056e71fbb2db817740ed870b33434e7a/grpcio-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8b39472beafc0bdcafc4c8c73ad082ebfdb449d566897a61e7acb4fa88089115", size = 6053264, upload-time = "2026-06-11T12:45:21.017Z" }, + { url = "https://files.pythonhosted.org/packages/e0/44/f257b7e0bd69c93b06c6cb8ac8d1b901ccb42bedabd83c1a4c77a71f8810/grpcio-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1e123f9b37edb8375fd74130d1f69c944bbf0a7b06761ae7211154b8759e94d2", size = 6595983, upload-time = "2026-06-11T12:45:26.963Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f3/19782aa04c960968bef8c5539329d8e3bbc3364e2e46d19eb5e5cc5e43b7/grpcio-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2c2e2ae6867c2966b8daccc836d54a13218e0007e9a490aeb81dd05be64d22d7", size = 7303455, upload-time = "2026-06-11T12:45:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8c/dea020b6d91508cd84463917a63149ec196ee7db505d032ae43fcb3303b9/grpcio-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:766bc7c9a9c340342f4c864ccbda8e78111e4751f13b895812b9c148fb79e9d0", size = 6809167, upload-time = "2026-06-11T12:45:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/3030dd940408083bd32cd95d634777a71605ade4887154d93e8a89244946/grpcio-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b259a04a737cb3496be0901328eb8b7552ed8df4865d8c8f1cf1bffcfc0776a3", size = 7412536, upload-time = "2026-06-11T12:45:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/e0/dd/1172a9e42b168edcafefad6115346ef619a3fc02158bb170e66ced24bcdd/grpcio-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:85b10a45b8993d195c4f3ff57025b8d1e11834909ee475c403bfa60cb4caefaf", size = 8408276, upload-time = "2026-06-11T12:45:37.78Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/71437c7f3596e5246155c515852795a85a1a8d228190212432b13b97a95d/grpcio-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8ea1936c26b99999b27479853039a7f34713f56c49375ad52b38535ec93a796c", size = 7849660, upload-time = "2026-06-11T12:45:40.627Z" }, ] [[package]] @@ -359,12 +360,12 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526 } +sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852 }, - { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250 }, - { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108 }, - { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216 }, + { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" }, ] [[package]] @@ -376,27 +377,27 @@ dependencies = [ { name = "omegaconf", marker = "sys_platform == 'linux'" }, { name = "packaging", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/0b/7c0d941311aadc6479ec01767edba9c8a07db1452685de3567ed3058d0c9/hydra_core-1.3.3.tar.gz", hash = "sha256:b7477ee21f08b62f71bf0126d44695c048dc7e9c0cc79e2d593b707cb1e44048", size = 3262532 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/0b/7c0d941311aadc6479ec01767edba9c8a07db1452685de3567ed3058d0c9/hydra_core-1.3.3.tar.gz", hash = "sha256:b7477ee21f08b62f71bf0126d44695c048dc7e9c0cc79e2d593b707cb1e44048", size = 3262532, upload-time = "2026-06-11T05:54:26.835Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/57/4e39f85347f77144d2ad12e87d5df8fb8f17023f9bd9e8c6e903a128382c/hydra_core-1.3.3-py3-none-any.whl", hash = "sha256:cf349fc393f486f250e5825592c3d0a50c0af3effd726cf8dd5b637a7cb464e3", size = 154706 }, + { url = "https://files.pythonhosted.org/packages/e5/57/4e39f85347f77144d2ad12e87d5df8fb8f17023f9bd9e8c6e903a128382c/hydra_core-1.3.3-py3-none-any.whl", hash = "sha256:cf349fc393f486f250e5825592c3d0a50c0af3effd726cf8dd5b637a7cb464e3", size = 154706, upload-time = "2026-06-11T05:54:24.917Z" }, ] [[package]] name = "idna" version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] @@ -406,9 +407,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] @@ -425,30 +426,30 @@ dependencies = [ { name = "packaging", marker = "sys_platform == 'linux'" }, { name = "rich", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/e7/97a7664581b73e4f9ff1d3a767a493b6ac5d3e0ed1926bd2b6b2c8bbccd7/keras-3.14.1.tar.gz", hash = "sha256:ef479173102ad29db89b53c232efdc3fb5ad57c28bc27ead59f3e78a1eecd05b", size = 1263647 } +sdist = { url = "https://files.pythonhosted.org/packages/35/e7/97a7664581b73e4f9ff1d3a767a493b6ac5d3e0ed1926bd2b6b2c8bbccd7/keras-3.14.1.tar.gz", hash = "sha256:ef479173102ad29db89b53c232efdc3fb5ad57c28bc27ead59f3e78a1eecd05b", size = 1263647, upload-time = "2026-05-07T21:43:35.112Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/03/184267c1d09783dd070f1ddfd0d4beb7503139dfc7bd75b422867cf282fd/keras-3.14.1-py3-none-any.whl", hash = "sha256:ebd2c14d2af3c9de18083604d408483996407fc7d2f9ebd1d565961f96608c29", size = 1628606 }, + { url = "https://files.pythonhosted.org/packages/02/03/184267c1d09783dd070f1ddfd0d4beb7503139dfc7bd75b422867cf282fd/keras-3.14.1-py3-none-any.whl", hash = "sha256:ebd2c14d2af3c9de18083604d408483996407fc7d2f9ebd1d565961f96608c29", size = 1628606, upload-time = "2026-05-07T21:43:32.737Z" }, ] [[package]] name = "libclang" version = "18.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/ca35e19a4f142adffa27e3d652196b7362fa612243e2b916845d801454fc/libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250", size = 39612 } +sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/ca35e19a4f142adffa27e3d652196b7362fa612243e2b916845d801454fc/libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250", size = 39612, upload-time = "2024-03-17T16:04:37.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b", size = 24515943 }, - { url = "https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592", size = 23784972 }, - { url = "https://files.pythonhosted.org/packages/fe/2f/d920822c2b1ce9326a4c78c0c2b4aa3fde610c7ee9f631b600acb5376c26/libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe", size = 20259606 }, - { url = "https://files.pythonhosted.org/packages/2d/c2/de1db8c6d413597076a4259cea409b83459b2db997c003578affdd32bf66/libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f", size = 24921494 }, + { url = "https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b", size = 24515943, upload-time = "2024-03-17T16:03:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592", size = 23784972, upload-time = "2024-03-17T16:12:47.677Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2f/d920822c2b1ce9326a4c78c0c2b4aa3fde610c7ee9f631b600acb5376c26/libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe", size = 20259606, upload-time = "2024-03-17T16:17:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c2/de1db8c6d413597076a4259cea409b83459b2db997c003578affdd32bf66/libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f", size = 24921494, upload-time = "2024-03-17T16:14:20.132Z" }, ] [[package]] name = "markdown" version = "3.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805 } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180 }, + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] @@ -458,32 +459,32 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454 } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687 }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -497,9 +498,9 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, { name = "urllib3", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113 } +sdist = { url = "https://files.pythonhosted.org/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113, upload-time = "2025-11-27T00:37:15.569Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751 }, + { url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" }, ] [[package]] @@ -509,15 +510,15 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314 } +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464 }, - { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002 }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, ] [[package]] name = "mlpstorage" -version = "3.0.15" +version = "3.0.16" source = { editable = "." } dependencies = [ { name = "dlio-benchmark", marker = "sys_platform == 'linux'" }, @@ -622,6 +623,7 @@ requires-dist = [ { name = "tabulate", marker = "extra == 'vectordb-milvus'", specifier = ">=0.9" }, { name = "tabulate", marker = "extra == 'vectordb-pgvector'", specifier = ">=0.9" }, ] +provides-extras = ["test", "full", "vectordb", "vectordb-milvus", "vectordb-pgvector", "vectordb-elasticsearch"] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=9.0.2" }] @@ -630,51 +632,51 @@ dev = [{ name = "pytest", specifier = ">=9.0.2" }] name = "mpi4py" version = "4.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/83/231445bbcf7ef10864746c244ff2d82000011449b79275642c5d4ed8c8f4/mpi4py-4.1.2.tar.gz", hash = "sha256:56860286dc45f20e8821e93cb06669e30462348bf866f685553fa4b712d58d02", size = 501709 } +sdist = { url = "https://files.pythonhosted.org/packages/75/83/231445bbcf7ef10864746c244ff2d82000011449b79275642c5d4ed8c8f4/mpi4py-4.1.2.tar.gz", hash = "sha256:56860286dc45f20e8821e93cb06669e30462348bf866f685553fa4b712d58d02", size = 501709, upload-time = "2026-05-16T10:35:23.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/0a/1da7f403e0d8ce0e26d541f7538302cec00cf5b0a98a7a52b929f938a25c/mpi4py-4.1.2-cp310-abi3-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2ef63b2e3083e6062fd90e4de8c4e3acbf81e0772406e0226eb8dde6a48cab8e", size = 1327130 }, - { url = "https://files.pythonhosted.org/packages/e6/f9/65999152ae82bad914c6a083821ee774afefd6d0544e633b940c9a9ebf3f/mpi4py-4.1.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6508e654b9c8ff9f611b19548b2a17d1e323b520a15168189f92221e6757b8ff", size = 1182268 }, - { url = "https://files.pythonhosted.org/packages/9f/2b/1e48c4c5f9acbdca8dd28beeba9123dde140cd2ca520f8e3a3cf22faeeaa/mpi4py-4.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00f4cce8999d19f35243c3442ea22debbe3336f69c309cd5d3176df4e51c717a", size = 1358844 }, - { url = "https://files.pythonhosted.org/packages/94/46/a37225d47997fcf30adca25d3849d035bbb61d972118b024db900306e528/mpi4py-4.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0a58e164776acb7b52414548b1bf0e5caafce0ee90345deff147873a64b6b2cc", size = 1227206 }, + { url = "https://files.pythonhosted.org/packages/a3/0a/1da7f403e0d8ce0e26d541f7538302cec00cf5b0a98a7a52b929f938a25c/mpi4py-4.1.2-cp310-abi3-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2ef63b2e3083e6062fd90e4de8c4e3acbf81e0772406e0226eb8dde6a48cab8e", size = 1327130, upload-time = "2026-05-16T10:34:00.269Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f9/65999152ae82bad914c6a083821ee774afefd6d0544e633b940c9a9ebf3f/mpi4py-4.1.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6508e654b9c8ff9f611b19548b2a17d1e323b520a15168189f92221e6757b8ff", size = 1182268, upload-time = "2026-05-16T10:34:02.206Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2b/1e48c4c5f9acbdca8dd28beeba9123dde140cd2ca520f8e3a3cf22faeeaa/mpi4py-4.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00f4cce8999d19f35243c3442ea22debbe3336f69c309cd5d3176df4e51c717a", size = 1358844, upload-time = "2026-05-16T10:34:28.247Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/a37225d47997fcf30adca25d3849d035bbb61d972118b024db900306e528/mpi4py-4.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0a58e164776acb7b52414548b1bf0e5caafce0ee90345deff147873a64b6b2cc", size = 1227206, upload-time = "2026-05-16T10:34:29.866Z" }, ] [[package]] name = "mpmath" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] name = "namex" version = "0.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/c0/ee95b28f029c73f8d49d8f52edaed02a1d4a9acb8b69355737fdb1faa191/namex-0.1.0.tar.gz", hash = "sha256:117f03ccd302cc48e3f5c58a296838f6b89c83455ab8683a1e85f2a430aa4306", size = 6649 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/c0/ee95b28f029c73f8d49d8f52edaed02a1d4a9acb8b69355737fdb1faa191/namex-0.1.0.tar.gz", hash = "sha256:117f03ccd302cc48e3f5c58a296838f6b89c83455ab8683a1e85f2a430aa4306", size = 6649, upload-time = "2025-05-26T23:17:38.918Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/bc/465daf1de06409cdd4532082806770ee0d8d7df434da79c76564d0f69741/namex-0.1.0-py3-none-any.whl", hash = "sha256:e2012a474502f1e2251267062aae3114611f07df4224b6e06334c57b0f2ce87c", size = 5905 }, + { url = "https://files.pythonhosted.org/packages/b2/bc/465daf1de06409cdd4532082806770ee0d8d7df434da79c76564d0f69741/namex-0.1.0-py3-none-any.whl", hash = "sha256:e2012a474502f1e2251267062aae3114611f07df4224b6e06334c57b0f2ce87c", size = 5905, upload-time = "2025-05-26T23:17:37.695Z" }, ] [[package]] name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504 }, + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] name = "numpy" version = "2.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807 } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012 }, - { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538 }, - { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706 }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541 }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, ] [[package]] @@ -685,8 +687,8 @@ dependencies = [ { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918 }, - { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758 }, + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, ] [[package]] @@ -694,8 +696,8 @@ name = "nvidia-cuda-cupti" version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827 }, - { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597 }, + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, ] [[package]] @@ -703,8 +705,8 @@ name = "nvidia-cuda-nvrtc" version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200 }, - { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449 }, + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, ] [[package]] @@ -712,8 +714,8 @@ name = "nvidia-cuda-runtime" version = "13.0.96" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060 }, - { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632 }, + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, ] [[package]] @@ -724,8 +726,8 @@ dependencies = [ { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296 }, - { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588 }, + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, ] [[package]] @@ -736,8 +738,8 @@ dependencies = [ { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554 }, - { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489 }, + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, ] [[package]] @@ -745,8 +747,8 @@ name = "nvidia-cufile" version = "1.15.1.6" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672 }, - { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992 }, + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, ] [[package]] @@ -754,8 +756,8 @@ name = "nvidia-curand" version = "10.4.0.35" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106 }, - { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258 }, + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, ] [[package]] @@ -768,8 +770,8 @@ dependencies = [ { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760 }, - { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980 }, + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, ] [[package]] @@ -780,8 +782,8 @@ dependencies = [ { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568 }, - { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937 }, + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, ] [[package]] @@ -789,8 +791,8 @@ name = "nvidia-cusparselt-cu13" version = "0.8.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344 }, - { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586 }, + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, ] [[package]] @@ -798,8 +800,8 @@ name = "nvidia-nccl-cu13" version = "2.29.7" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712 }, - { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000 }, + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, ] [[package]] @@ -807,8 +809,8 @@ name = "nvidia-nvjitlink" version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933 }, - { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748 }, + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, ] [[package]] @@ -816,8 +818,8 @@ name = "nvidia-nvshmem-cu13" version = "3.4.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947 }, - { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546 }, + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, ] [[package]] @@ -825,8 +827,8 @@ name = "nvidia-nvtx" version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047 }, - { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, ] [[package]] @@ -837,18 +839,18 @@ dependencies = [ { name = "antlr4-python3-runtime", marker = "sys_platform == 'linux'" }, { name = "pyyaml", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472, upload-time = "2026-06-11T05:05:12.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502 }, + { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502, upload-time = "2026-06-11T05:05:09.954Z" }, ] [[package]] name = "opt-einsum" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004, upload-time = "2024-09-26T14:33:24.483Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932 }, + { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932, upload-time = "2024-09-26T14:33:23.039Z" }, ] [[package]] @@ -858,41 +860,41 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/63/92328a17ab7836562fe0129e605f685a88db35ce98427c34ff48ee4ec157/optree-0.19.1.tar.gz", hash = "sha256:4497d1c9197b8c6842e511368163d318ce536521ebdcff8bebb7551dcdfac532", size = 177531 } +sdist = { url = "https://files.pythonhosted.org/packages/44/63/92328a17ab7836562fe0129e605f685a88db35ce98427c34ff48ee4ec157/optree-0.19.1.tar.gz", hash = "sha256:4497d1c9197b8c6842e511368163d318ce536521ebdcff8bebb7551dcdfac532", size = 177531, upload-time = "2026-05-06T02:32:39.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/1e/676470909aa64d7aba7c5edf83b171dc83b7af901d9ebb8e6d7512fe913a/optree-0.19.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a1202371d9fe3aa75f3e886b1f871aac4991a655aadb65e54f58a3ae9388ab2", size = 413157 }, - { url = "https://files.pythonhosted.org/packages/f4/41/1a4c58f2af5742b9d9e21ea9e45c6c3c49463b5e2a0537e84ead1e9597ca/optree-0.19.1-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d41ccc4c20bfeae01d1d221c057a6d026e84e32229664952eddcdbe4b9b71417", size = 476923 }, - { url = "https://files.pythonhosted.org/packages/10/c1/f62167bd9d6f6c948b191a0943923404678d47100f777f4a8fb37816e6f8/optree-0.19.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d934f240b109c6891dd06b2e30400b123b8a4b6ed31dcd0db2ae2378d30a6e8", size = 475385 }, - { url = "https://files.pythonhosted.org/packages/30/5e/5323c5fa3024fdd900bdd8f14621139ed844c2247bf1a26e7cf5c1116188/optree-0.19.1-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ddeefb7ca799c09647e332ebc1a5f6c09888a5a0e51f2dff4ca55e65b42a8c14", size = 474406 }, - { url = "https://files.pythonhosted.org/packages/e2/6a/54e4c47e61a51504a5224c933722e0c8a69925aacec4c08175e9675aeb81/optree-0.19.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0ce49f64f804f7f35f2f9c2a21e3ba94c090199fccdcfd40e3ded4426c5c175", size = 457596 }, - { url = "https://files.pythonhosted.org/packages/a7/12/bba07c0b769586c6bd54e81f1f734cad103dbe30abbadee940fe7d3e330e/optree-0.19.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e0f02600832ab8d0f6c934dcb5c339e17a36938d477641a45798e02625ebe107", size = 417900 }, + { url = "https://files.pythonhosted.org/packages/c2/1e/676470909aa64d7aba7c5edf83b171dc83b7af901d9ebb8e6d7512fe913a/optree-0.19.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a1202371d9fe3aa75f3e886b1f871aac4991a655aadb65e54f58a3ae9388ab2", size = 413157, upload-time = "2026-05-06T02:31:00.339Z" }, + { url = "https://files.pythonhosted.org/packages/f4/41/1a4c58f2af5742b9d9e21ea9e45c6c3c49463b5e2a0537e84ead1e9597ca/optree-0.19.1-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d41ccc4c20bfeae01d1d221c057a6d026e84e32229664952eddcdbe4b9b71417", size = 476923, upload-time = "2026-05-06T02:31:01.492Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/f62167bd9d6f6c948b191a0943923404678d47100f777f4a8fb37816e6f8/optree-0.19.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d934f240b109c6891dd06b2e30400b123b8a4b6ed31dcd0db2ae2378d30a6e8", size = 475385, upload-time = "2026-05-06T02:31:02.836Z" }, + { url = "https://files.pythonhosted.org/packages/30/5e/5323c5fa3024fdd900bdd8f14621139ed844c2247bf1a26e7cf5c1116188/optree-0.19.1-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ddeefb7ca799c09647e332ebc1a5f6c09888a5a0e51f2dff4ca55e65b42a8c14", size = 474406, upload-time = "2026-05-06T02:31:04.023Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/54e4c47e61a51504a5224c933722e0c8a69925aacec4c08175e9675aeb81/optree-0.19.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0ce49f64f804f7f35f2f9c2a21e3ba94c090199fccdcfd40e3ded4426c5c175", size = 457596, upload-time = "2026-05-06T02:31:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/a7/12/bba07c0b769586c6bd54e81f1f734cad103dbe30abbadee940fe7d3e330e/optree-0.19.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e0f02600832ab8d0f6c934dcb5c339e17a36938d477641a45798e02625ebe107", size = 417900, upload-time = "2026-05-06T02:31:07.251Z" }, ] [[package]] name = "orjson" version = "3.11.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106 }, - { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864 }, - { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213 }, - { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994 }, - { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744 }, - { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014 }, - { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509 }, - { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127 }, - { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025 }, - { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943 }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, ] [[package]] name = "packaging" version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195 }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -903,12 +905,12 @@ dependencies = [ { name = "numpy", marker = "sys_platform == 'linux'" }, { name = "python-dateutil", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965 }, - { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600 }, - { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824 }, - { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889 }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, ] [[package]] @@ -918,107 +920,107 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354 } +sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441 }, + { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" }, ] [[package]] name = "pillow" version = "12.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462 }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744 }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371 }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215 }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783 }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112 }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, + { 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" }, ] [[package]] name = "protobuf" version = "7.35.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717 } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847 }, - { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030 }, - { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130 }, - { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659 }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, ] [[package]] name = "psutil" version = "7.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560 }, - { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997 }, - { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972 }, - { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266 }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, ] [[package]] name = "psycopg2-binary" version = "2.9.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686 } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676 }, - { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917 }, - { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843 }, - { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556 }, - { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714 }, - { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154 }, - { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882 }, - { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298 }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, ] [[package]] name = "pyarrow" version = "24.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261 } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394 }, - { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122 }, - { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032 }, - { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490 }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pycryptodome" version = "3.23.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625 }, - { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954 }, - { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534 }, - { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853 }, - { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465 }, - { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414 }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, ] [[package]] @@ -1031,9 +1033,9 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, { name = "typing-inspection", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775 } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262 }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] @@ -1043,36 +1045,38 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742 }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418 }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274 }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940 }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516 }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854 }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306 }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044 }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133 }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464 }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] [[package]] name = "pydftracer" version = "2.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/12/b7f0bfb3888d569e630c110d977b00f0fa010e51ffc667524d7ecf0affea/pydftracer-2.0.2.tar.gz", hash = "sha256:3a2d92e17206e5a69f8e890b00b087943372680755c5e6c5e6e2b7b0814f5e92", size = 45448 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/12/b7f0bfb3888d569e630c110d977b00f0fa010e51ffc667524d7ecf0affea/pydftracer-2.0.2.tar.gz", hash = "sha256:3a2d92e17206e5a69f8e890b00b087943372680755c5e6c5e6e2b7b0814f5e92", size = 45448, upload-time = "2025-10-20T06:09:20.566Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/8e/4c9cde902dbac10227dff0975e6d8ce6eab70358f4db38862fce2939d1c3/pydftracer-2.0.2-py3-none-any.whl", hash = "sha256:29962597d301387698be901137c62c4569635b05975e982904df63e19197df93", size = 18683 }, + { url = "https://files.pythonhosted.org/packages/c6/8e/4c9cde902dbac10227dff0975e6d8ce6eab70358f4db38862fce2939d1c3/pydftracer-2.0.2-py3-none-any.whl", hash = "sha256:29962597d301387698be901137c62c4569635b05975e982904df63e19197df93", size = 18683, upload-time = "2025-10-20T06:09:19.651Z" }, ] [[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] @@ -1090,7 +1094,7 @@ dependencies = [ { name = "setuptools", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/c1/01647e61f3a82fd881382746b6dde3401d65b88cd4f75bd059901fb2392b/pymilvus-3.0.0-1-py3-none-any.whl", hash = "sha256:57c8e7c87fbbf579f122b4df893949bc78e50bca2988527864891bd544817b05", size = 344817 }, + { url = "https://files.pythonhosted.org/packages/ac/c1/01647e61f3a82fd881382746b6dde3401d65b88cd4f75bd059901fb2392b/pymilvus-3.0.0-1-py3-none-any.whl", hash = "sha256:57c8e7c87fbbf579f122b4df893949bc78e50bca2988527864891bd544817b05", size = 344817, upload-time = "2026-05-07T14:57:45.235Z" }, ] [[package]] @@ -1103,9 +1107,9 @@ dependencies = [ { name = "pluggy", marker = "sys_platform == 'linux'" }, { name = "pygments", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181 } +sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453 }, + { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, ] [[package]] @@ -1117,9 +1121,9 @@ dependencies = [ { name = "pluggy", marker = "sys_platform == 'linux'" }, { name = "pytest", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876 }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -1129,9 +1133,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036 } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095 }, + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] @@ -1141,31 +1145,31 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135 } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101 }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, ] [[package]] @@ -1178,9 +1182,9 @@ dependencies = [ { name = "idna", marker = "sys_platform == 'linux'" }, { name = "urllib3", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075 }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -1191,9 +1195,9 @@ dependencies = [ { name = "markdown-it-py", marker = "sys_platform == 'linux'" }, { name = "pygments", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654 }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] @@ -1203,10 +1207,10 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/98/23ed0451a8668e352206dea740920d85dceefadf0a6d427d1571d17e845e/s3dlio-0.9.100.tar.gz", hash = "sha256:b2d3dc9f037bcef5e2e171ab1988c1be730849730bee6570f484eb0f02c9a862", size = 1564701 } +sdist = { url = "https://files.pythonhosted.org/packages/33/98/23ed0451a8668e352206dea740920d85dceefadf0a6d427d1571d17e845e/s3dlio-0.9.100.tar.gz", hash = "sha256:b2d3dc9f037bcef5e2e171ab1988c1be730849730bee6570f484eb0f02c9a862", size = 1564701, upload-time = "2026-05-13T05:08:22.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/80/e7a16ae10aa9374b29ae7dc175eaba3910f604c2f2d2ae8955488a13c821/s3dlio-0.9.100-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:090f61effc0eec32a876a62a921287961e92aec57eb0f21449bf5a89d9e9ada2", size = 12416760 }, - { url = "https://files.pythonhosted.org/packages/ce/38/44ad05689f5f66e503eb095b442f37271e74bde1948fadf1312284173ae3/s3dlio-0.9.100-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb48f3d295071b5226ad6062544003abaa2defadac695424a015db04126f5d57", size = 12842294 }, + { url = "https://files.pythonhosted.org/packages/3b/80/e7a16ae10aa9374b29ae7dc175eaba3910f604c2f2d2ae8955488a13c821/s3dlio-0.9.100-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:090f61effc0eec32a876a62a921287961e92aec57eb0f21449bf5a89d9e9ada2", size = 12416760, upload-time = "2026-05-13T05:08:10.756Z" }, + { url = "https://files.pythonhosted.org/packages/ce/38/44ad05689f5f66e503eb095b442f37271e74bde1948fadf1312284173ae3/s3dlio-0.9.100-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb48f3d295071b5226ad6062544003abaa2defadac695424a015db04126f5d57", size = 12842294, upload-time = "2026-05-13T05:08:13.756Z" }, ] [[package]] @@ -1217,43 +1221,43 @@ dependencies = [ { name = "s3torchconnectorclient", marker = "sys_platform == 'linux'" }, { name = "torch", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/24/a3422bc7e3d8f2a55a64250a6d5a07416c49d6f5695879445ff72c695612/s3torchconnector-1.5.0.tar.gz", hash = "sha256:44167d8e7bc0fce6d97627fc10aa7e215f4b58e0bb7037e87858c41eefd5b5af", size = 103050 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/24/a3422bc7e3d8f2a55a64250a6d5a07416c49d6f5695879445ff72c695612/s3torchconnector-1.5.0.tar.gz", hash = "sha256:44167d8e7bc0fce6d97627fc10aa7e215f4b58e0bb7037e87858c41eefd5b5af", size = 103050, upload-time = "2026-02-20T13:05:41.437Z" } [[package]] name = "s3torchconnectorclient" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/8d/e04febe3e7ff7c91bc4678a16bec1c87674fc9c160c75a8f8745e516e563/s3torchconnectorclient-1.5.0.tar.gz", hash = "sha256:09ffceca1fd025abd8a4a4cbd94b3f70a7c8ccfbf3e0f76337e180f95ce58e61", size = 85516 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/8d/e04febe3e7ff7c91bc4678a16bec1c87674fc9c160c75a8f8745e516e563/s3torchconnectorclient-1.5.0.tar.gz", hash = "sha256:09ffceca1fd025abd8a4a4cbd94b3f70a7c8ccfbf3e0f76337e180f95ce58e61", size = 85516, upload-time = "2026-02-20T13:05:42.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/51/288b8857991cffa36b833c7128897766fb84f3a4a60a5cc3dfe6e2546f8a/s3torchconnectorclient-1.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7c0d11b4da0271414ffa370718bbbfb5454dac2ad546d89c7c6c49831e2eb7e5", size = 3594664 }, - { url = "https://files.pythonhosted.org/packages/35/d3/9354e5620c3839393ff9afe2435f5e42bb63eb829edd93395cb0a3b1aa39/s3torchconnectorclient-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f5277d76b4d1e12cd6f96823cf5911c51a7a614acbabb4ee4133d8caa332df1", size = 3747379 }, + { url = "https://files.pythonhosted.org/packages/7d/51/288b8857991cffa36b833c7128897766fb84f3a4a60a5cc3dfe6e2546f8a/s3torchconnectorclient-1.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7c0d11b4da0271414ffa370718bbbfb5454dac2ad546d89c7c6c49831e2eb7e5", size = 3594664, upload-time = "2026-02-20T13:05:15.708Z" }, + { url = "https://files.pythonhosted.org/packages/35/d3/9354e5620c3839393ff9afe2435f5e42bb63eb829edd93395cb0a3b1aa39/s3torchconnectorclient-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f5277d76b4d1e12cd6f96823cf5911c51a7a614acbabb4ee4133d8caa332df1", size = 3747379, upload-time = "2026-02-20T13:05:17.76Z" }, ] [[package]] name = "setuptools" version = "81.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021 }, + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] @@ -1263,18 +1267,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] name = "tabulate" version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754 } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814 }, + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, ] [[package]] @@ -1294,7 +1298,7 @@ dependencies = [ { name = "werkzeug", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, ] [[package]] @@ -1302,8 +1306,8 @@ name = "tensorboard-data-server" version = "0.7.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356 }, - { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363 }, + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, ] [[package]] @@ -1334,17 +1338,17 @@ dependencies = [ { name = "wrapt", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/b4/f028a5de27d0fda10ba6145bc76e40c37ff6d2d1e95b601adb5ae17d635e/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bfbfb3dd0e22bffc45fe1e922390d27753e99261fab8a882e802cf98a0e078f", size = 259533109 }, - { url = "https://files.pythonhosted.org/packages/9c/d1/6aa15085d672056d5f08b5f28b1c7ce01c4e12149a23b0c98e3c79d04441/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25265b0bc527e0d54b1e9cc60c44a24f44a809fe27666b905f0466471f9c52ec", size = 620682547 }, + { url = "https://files.pythonhosted.org/packages/ec/b4/f028a5de27d0fda10ba6145bc76e40c37ff6d2d1e95b601adb5ae17d635e/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bfbfb3dd0e22bffc45fe1e922390d27753e99261fab8a882e802cf98a0e078f", size = 259533109, upload-time = "2025-08-13T16:52:31.513Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d1/6aa15085d672056d5f08b5f28b1c7ce01c4e12149a23b0c98e3c79d04441/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25265b0bc527e0d54b1e9cc60c44a24f44a809fe27666b905f0466471f9c52ec", size = 620682547, upload-time = "2025-08-13T16:52:46.396Z" }, ] [[package]] name = "termcolor" version = "3.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434 } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734 }, + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] [[package]] @@ -1369,8 +1373,8 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751 }, - { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691 }, + { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751, upload-time = "2026-05-13T14:55:03.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691, upload-time = "2026-05-13T14:52:54.453Z" }, ] [[package]] @@ -1378,17 +1382,17 @@ name = "triton" version = "3.7.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104 }, - { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567 }, + { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567, upload-time = "2026-05-07T18:46:13.505Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] @@ -1398,18 +1402,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "urllib3" version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602 } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087 }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] @@ -1419,9 +1423,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852 } +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459 }, + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] @@ -1431,42 +1435,42 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854 } +sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218 }, + { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, ] [[package]] name = "wrapt" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e", size = 169364 }, - { url = "https://files.pythonhosted.org/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926", size = 171079 }, - { url = "https://files.pythonhosted.org/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624", size = 160205 }, - { url = "https://files.pythonhosted.org/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710", size = 168922 }, - { url = "https://files.pythonhosted.org/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f", size = 158388 }, - { url = "https://files.pythonhosted.org/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797", size = 167682 }, - { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000 }, + { url = "https://files.pythonhosted.org/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e", size = 169364, upload-time = "2026-05-22T14:48:02.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926", size = 171079, upload-time = "2026-05-22T14:48:04.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624", size = 160205, upload-time = "2026-05-22T14:48:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710", size = 168922, upload-time = "2026-05-22T14:48:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f", size = 158388, upload-time = "2026-05-22T14:48:08.629Z" }, + { url = "https://files.pythonhosted.org/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797", size = 167682, upload-time = "2026-05-22T14:48:10.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, ] [[package]] name = "zstandard" version = "0.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019 }, - { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012 }, - { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148 }, - { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652 }, - { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993 }, - { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806 }, - { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659 }, - { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933 }, - { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008 }, - { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517 }, - { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292 }, - { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237 }, +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, ]