diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1553e7..e1f9f4d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `vspec compose` command: bundles a vspec model and all its dependencies (includes, overlays, units, quantities) into a self-contained snapshot folder. Validates the full expanded model before writing, then serialises the raw authored content faithfully — preserving `instances`, instance-level overrides, and custom struct types. See [compose.md](docs/compose.md). +- `vspec diff` command: compares two compose snapshots and reports every change (ADDED, REMOVED, MODIFIED) as structured JSON. Detects renames via the `fka` field and cascades them to child nodes automatically. See [diff.md](docs/diff.md). + ### Changed - **Breaking**: Unit descriptions (`unit` field) must now be globally unique across all quantities. Previously accepted duplicate unit descriptions in `units.yaml` will now fail validation with a clear error message identifying the conflicting unit keys. @@ -40,7 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Major restructure of repository structure and CLI The vss-tools repository content structure and CLI has changed significantly -For more information see [vspec documentation](docs/vspec.md) +For more information see [vspec documentation](docs/export.md) ### Change in UUID handling. @@ -106,7 +111,7 @@ In VSS-Tools 4.0 structs are supported in the following exporters: Other exporters do not support structs. It is possible to use specify muliple type files with `--types`, and to use types in combination with overlays. -For more information see [vspec documentation](docs/vspec.md) +For more information see [vspec documentation](docs/export.md) ### Change in UUID handling. @@ -133,7 +138,7 @@ From now on, if new units are needed for the VSS catalog they shall be added to Support for defining signals with struct type added. For VSS 3.1 as experimental feature only supported by JSON exporter. -For more information see [vspec documentation](docs/vspec.md) +For more information see [vspec documentation](docs/export.md) ### Change in UUID handling. @@ -155,7 +160,7 @@ The background is that they have been broken for a long period and no one has vo ### Support for specifying unit files -Add new parameter `-u` has been introduced, see [documentation](https://github.com/COVESA/vss-tools/blob/master/docs/vspec.md#handling-of-units). +Add new parameter `-u` has been introduced, see [documentation](https://github.com/COVESA/vss-tools/blob/master/docs/export.md#handling-of-units). Use of default unit file deprecated. At the same time a unit file has been added to [VSS](https://github.com/COVESA/vehicle_signal_specification/blob/master/spec/units.yaml), allowing VSS tooling to control their own units rather than relying on units in VSS-tools. @@ -169,4 +174,4 @@ file it will be used, only if not existing `config.yaml` in vss-tools will be us ### Overlay Support Overlays introduced to allow customization of VSS. See [documentation](https://covesa.github.io/vehicle_signal_specification/rule_set/overlay/). -See [vss-tools documentation](https://github.com/COVESA/vss-tools/blob/master/docs/vspec.md) on how to include overlays when transforming VSS. +See [vss-tools documentation](https://github.com/COVESA/vss-tools/blob/master/docs/export.md) on how to include overlays when transforming VSS. diff --git a/FAQ.md b/FAQ.md index 7d6b91c1..c5dab71e 100644 --- a/FAQ.md +++ b/FAQ.md @@ -5,7 +5,7 @@ VSS standard catalog uses the units defined in [this file](https://github.com/COVESA/vehicle_signal_specification/blob/master/spec/units.yaml). They are also described in the [VSS documentation](https://github.com/COVESA/vehicle_signal_specification/blob/master/docs-gen/content/rule_set/data_entry/data_units.md). -It is possible to replace the default units by the `--units/-u` parameter, see [vspec documentation](docs/vspec.md). +It is possible to replace the default units by the `--units/-u` parameter, see [vspec documentation](docs/export.md). The syntax of the file with units shall follow this pattern: diff --git a/README.md b/README.md index b6d9643d..a62231e4 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,17 @@ vspec export --help vspec export json --help ``` -Please check [here](./docs/vspec.md) for generic info about exporters and their arguments +Please check [here](./docs/export.md) for generic info about exporters and their arguments as well as [here](./docs/vspec_arch.md) for design decision, architecture and limitations. +vss-tools provides three top-level commands: + +| Command | Description | Documentation | +|---|---|---| +| `vspec export` | Convert a vspec model to a target format (JSON, YAML, Protobuf, …) | [export.md](./docs/export.md) | +| `vspec compose` | Bundle a vspec model and all its dependencies into a self-contained snapshot folder | [compose.md](./docs/compose.md) | +| `vspec diff` | Compare two compose snapshots and report every change as structured JSON | [diff.md](./docs/diff.md) | + ## Compatibility with VSS The [COVESA VSS project repository](https://github.com/COVESA/vehicle_signal_specification) includes vss-tools as a submodule. diff --git a/docs/compose.md b/docs/compose.md new file mode 100644 index 00000000..41e99384 --- /dev/null +++ b/docs/compose.md @@ -0,0 +1,69 @@ +# compose + +## What it does + +A vspec model is typically spread across many files: a main `.vspec`, optional `#include` files, overlays, a units file, and a quantities file. `vspec compose` collapses all of that into a single **self-contained folder** (a _snapshot_) that any downstream `vspec export` command can consume without needing the original source tree. + +``` +vspec compose \ + -u units.yaml \ + -q quantities.yaml \ + -s spec/Vehicle.vspec \ + -l my_overlay.vspec \ + --types types.vspec \ + -o snapshot/ +``` + +The output folder contains up to four files: + +| File | Contains | Always written? | +|---|---|---| +| `model_snapshot.vspec` | The full model tree | Yes | +| `structs_snapshot.vspec` | Custom struct type definitions | Only when `--types` is given | +| `units_snapshot.yaml` | Resolved units | When `-u` is given or `units.yaml` exists next to the vspec | +| `quantities_snapshot.yaml` | Resolved quantities | When `-q` is given or `quantities.yaml` exists next to the vspec | + +> Note: `vss-tools` automatically looks for `units.yaml` and `quantities.yaml` next to the vspec file when no `-u`/`-q` flags are given. If neither explicit paths nor default files can be found, the corresponding snapshot files are simply not written. + + +## How it works + +```mermaid +flowchart TD + A[Source .vspec + overlays + units/quantities] --> B[Phase 1: validate\nget_trees expand=True] + B -->|invalid model| C[Abort with error] + B -->|valid| D[Phase 2: raw serialization\nload_vspec direct] + D --> E[snapshot/model_snapshot.vspec] + D --> F[snapshot/structs_snapshot.vspec] + D --> G[snapshot/units_snapshot.yaml] + D --> H[snapshot/quantities_snapshot.yaml] +``` + +**Phase 1 — Validation.** The full model is loaded with expansion enabled. All Pydantic type checks, unit validation, naming conventions, and structural rules run normally. If anything is wrong the command aborts with a clear error — the snapshot is never written. + +**Phase 2 — Faithful serialization.** The raw authored YAML is read directly (without tree-building) and written as-is. This means: + +- `instances` fields are preserved — the snapshot is not pre-expanded. +- Instance-level overrides (e.g. `Door.Row1.Left.IsOpen`) written in the source are kept exactly as authored. +- Only internal runtime fields (`delete`, `fqn`, `is_instance`) are stripped. +- No default values are injected — what you wrote is what you get. + +## Using the snapshot + +The primary use of such a snapshot could be a released artifact to complement the versioning and evolution of a domain data model. +Once the snapshot is taken, it becomes an inmutable reference. +Then, it becomes easier to report differences between two snapshopts. +See [diff command](./diff.md). + +Because the snapshot is valid vspec, it feeds directly into any exporter: + +```bash +vspec export json \ + -u snapshot/units_snapshot.yaml \ + -q snapshot/quantities_snapshot.yaml \ + -s snapshot/model_snapshot.vspec \ + --types snapshot/structs_snapshot.vspec \ + -o output.json +``` + +This also means the snapshot round-trips cleanly: composing a snapshot of a snapshot produces identical output. \ No newline at end of file diff --git a/docs/diff.md b/docs/diff.md new file mode 100644 index 00000000..2987492a --- /dev/null +++ b/docs/diff.md @@ -0,0 +1,113 @@ +# diff + +## What it does + +`vspec diff` compares two snapshots produced by `vspec compose` and reports every change as a structured JSON document. It is the foundation for tracking model evolution, generating changelogs, and feeding a spec registry. + +```bash +vspec diff -p snapshot_v1/ -c snapshot_v2/ +# or write to file: +vspec diff -p snapshot_v1/ -c snapshot_v2/ -o changes.json +``` + +## Output structure + +```json +{ + "previous": "snapshot_v1/", + "current": "snapshot_v2/", + "summary": { "ADDED": 2, "REMOVED": 1, "MODIFIED": 3 }, + "changes": [ ... ] +} +``` + +Each entry in `changes` has: + +| Field | Always present | Description | +|---|---|---| +| `type` | Yes | `ADDED`, `REMOVED`, or `MODIFIED` | +| `source` | Yes | `model`, `structs`, `units`, or `quantities` | +| `path` | Yes | FQN of the node in the **current** snapshot | +| `node_type` | Yes | e.g. `branch`, `sensor`, `actuator` | +| `message` | Yes | Human-readable description of the change | +| `previous_path` | MODIFIED renames only | FQN in the previous snapshot | +| `cascade` | MODIFIED renames only | `true` if this is a child of a renamed branch | +| `attribute_changes` | MODIFIED only | List of `{attribute, previous, current}` dicts | +| `attributes` | ADDED only | Full attribute dict of the new node | + +## Change types + +**ADDED** — a node exists in current but not in previous. + +**REMOVED** — a node exists in previous but not in current. + +**MODIFIED** — a node exists in both, but something changed. This covers: +- attribute value changes (datatype, unit, description, etc.) +- renames detected via the `fka` ("formerly known as") field + +## Rename detection + +Renames are detected automatically without requiring any extra flags. + +```mermaid +flowchart LR + subgraph previous + A[A.Door\ntype: branch] + B[A.Door.IsOpen\ntype: actuator] + end + subgraph current + C["A.Portal\ntype: branch\nfka: A.Door"] + D[A.Portal.IsOpen\ntype: actuator] + end + A -- "fka match + same node_type" --> C + B -- "prefix substitution\ncascade: true" --> D +``` + +1. **Explicit rename** — if an added node has `fka: [old.path]` and the same `type` as the removed node, it is reported as `MODIFIED` with `previous_path`. +2. **Cascade** — children of a renamed branch are matched by FQN prefix substitution. Each cascaded child is independently checked for attribute changes too. + +If `fka` is missing, or the node type doesn't match, the pair is reported as independent `REMOVED` + `ADDED`. + +## Example output + +```json +{ + "type": "MODIFIED", + "source": "model", + "path": "A.Portal", + "previous_path": "A.Door", + "node_type": "branch", + "cascade": false, + "attribute_changes": [], + "message": "Branch 'A.Door' was renamed to 'A.Portal'." +}, +{ + "type": "MODIFIED", + "source": "model", + "path": "A.Portal.IsOpen", + "previous_path": "A.Door.IsOpen", + "node_type": "actuator", + "cascade": true, + "attribute_changes": [ + { "attribute": "datatype", "previous": "boolean", "current": "string" } + ], + "message": "Actuator 'A.Door.IsOpen' was renamed to 'A.Portal.IsOpen' (cascaded from parent rename). Attribute 'datatype' changed from 'boolean' to 'string'." +} +``` + +## Typical workflow + +```mermaid +flowchart LR + S[Source files\nvspec + overlays] --> C1[vspec compose\nv1.0] + S2[Updated source files] --> C2[vspec compose\nv2.0] + C1 --> SN1[snapshot_v1/] + C2 --> SN2[snapshot_v2/] + SN1 --> D[vspec diff] + SN2 --> D + D --> J[changes.json\nfed to registry / changelog] +``` + +## About braking changes +The `diff` command is intentionally reporting ANY change without dictating what constitudes a braking chage. +That distinction is to be handle elsewhere based on the diff report. \ No newline at end of file diff --git a/docs/vspec.md b/docs/export.md similarity index 99% rename from docs/vspec.md rename to docs/export.md index e667592c..2ebae537 100644 --- a/docs/vspec.md +++ b/docs/export.md @@ -100,7 +100,7 @@ Instead instance information will be kept as additional information for the bran ### -e, --extended-attributes -See section on [overlays](vspec.md#handling-of-overlays-and-extensions) below +See section on [overlays](export.md#handling-of-overlays-and-extensions) below ### --strict-exceptions diff --git a/docs/samm.md b/docs/samm.md index 60de3461..bc046bf5 100644 --- a/docs/samm.md +++ b/docs/samm.md @@ -204,12 +204,12 @@ Also, please keep in mind that since the CLI tool also provides functionality to ### Running this exporter in DEBUG or other mode -As per available functionality, provided by the [vspec](vspec.md), the DEFAULT mode of execution of this and other exporters is INFO. +As per available functionality, provided by the [vspec](export.md), the DEFAULT mode of execution of this and other exporters is INFO. Other possible modes are: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL". -In order to switch these when calling this exporter you can use the option: [--log-level](vspec.md#--log-level). -Also there is an option to redirect the console output i.e. logged information to a text file. To do so, you can use the [--log-file](vspec.md#--log-file) option. +In order to switch these when calling this exporter you can use the option: [--log-level](export.md#--log-level). +Also there is an option to redirect the console output i.e. logged information to a text file. To do so, you can use the [--log-file](export.md#--log-file) option. A complete example, where you can call this exporter in DEBUG mode and store the logged information into a simple text file would be: diff --git a/noxfile.py b/noxfile.py index 138149e2..1b76fc85 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,6 @@ import nox -PYTHON_VERSIONS = ["3.10", "3.13"] +PYTHON_VERSIONS = ["3.11", "3.13"] nox.options.default_venv_backend = "uv" diff --git a/pyproject.toml b/pyproject.toml index c15b8e19..3bf8298d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "vss-tools" version = "7.0.0-dev0" description='COVESA Vehicle Signal Specification tooling.' readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "anytree>=2.12.1", "click>=8.1.7", @@ -18,6 +18,7 @@ dependencies = [ "pandas>=2.2.3", "case-converter>=1.2.0", "inflect>=7.5.0", + "ddmodl", ] authors = [ {name="COVESA VSS", email="covesa-dev@covesa.global"} @@ -45,6 +46,7 @@ line-length = 120 [tool.uv] package = true + [tool.ruff.lint] select = ["E", "F", "I"] diff --git a/src/vss_tools/cli.py b/src/vss_tools/cli.py index 129ee5d2..151fc080 100644 --- a/src/vss_tools/cli.py +++ b/src/vss_tools/cli.py @@ -16,7 +16,15 @@ from vss_tools.lazy_group import LazyGroup -@click.group(context_settings={"auto_envvar_prefix": "vss_tools"}, invoke_without_command=True) +@click.group( + cls=LazyGroup, + lazy_subcommands={ + "compose": "vss_tools.compose:cli", + "diff": "vss_tools.diff_cmd:cli", + }, + context_settings={"auto_envvar_prefix": "vss_tools"}, + invoke_without_command=True, +) @clo.log_level_opt @clo.log_file_opt @click.version_option() diff --git a/src/vss_tools/compose.py b/src/vss_tools/compose.py new file mode 100644 index 00000000..d9e03020 --- /dev/null +++ b/src/vss_tools/compose.py @@ -0,0 +1,172 @@ +# Copyright (c) 2026 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0 which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +# +# Composes a vspec model (potentially spread across many files and overlays) +# into a stand-alone output folder containing: +# - model_snapshot.vspec — the flat, non-expanded signal tree +# - structs_snapshot.vspec — the flat types tree (only written when --types is given) +# +# Both files are plain YAML and can be fed back into any vspec exporter as: +# vspec export -s /model_snapshot.vspec \ +# --types /structs_snapshot.vspec ... +# + +from pathlib import Path +from typing import Any + +import rich_click as click +import yaml + +import vss_tools.cli_options as clo +from vss_tools import log +from vss_tools.exporters.yaml import export_yaml +from vss_tools.main import get_trees +from vss_tools.units_quantities import load_quantities, load_units +from vss_tools.vspec import load_vspec + +# Fields to drop during compose serialization. +# Keeps instantiate/aggregate/arraysize (unlike the normal export) so the +# output remains a valid, re-parseable vspec. +SNAPSHOT_EXCLUDE_FIELDS = ["delete", "fqn", "is_instance"] + +MODEL_SNAPSHOT_FILENAME = "model_snapshot.vspec" +STRUCTS_SNAPSHOT_FILENAME = "structs_snapshot.vspec" +UNITS_SNAPSHOT_FILENAME = "units_snapshot.yaml" +QUANTITIES_SNAPSHOT_FILENAME = "quantities_snapshot.yaml" + + +def _filter_raw_node(attrs: dict[str, Any], exclude_fields: list[str]) -> dict[str, Any]: + """Strip internal-only fields and empty/null values from a raw authored node dict.""" + return {k: v for k, v in attrs.items() if k not in exclude_fields and v is not None and v != []} + + +def _resolve_paths(files: tuple[Path, ...], default: Path) -> list[Path]: + """Return resolved file list, falling back to default if present and none given.""" + if files: + return list(files) + if default.exists(): + return [default] + return [] + + +@click.command() +@clo.vspec_opt +@clo.include_dirs_opt +@clo.extended_attributes_opt +@clo.strict_opt +@clo.aborts_opt +@clo.overlays_opt +@clo.quantities_opt +@clo.units_opt +@clo.types_opt +@clo.strict_exceptions_opt +@click.option( + "--output-dir", + "-o", + required=True, + type=click.Path(file_okay=False, writable=True, path_type=Path), + help="Output directory. Created if it does not exist.", +) +def cli( + vspec: Path, + include_dirs: tuple[Path], + extended_attributes: tuple[str], + strict: bool, + aborts: tuple[str], + overlays: tuple[Path], + quantities: tuple[Path], + units: tuple[Path], + types: tuple[Path], + strict_exceptions: Path | None, + output_dir: Path, +) -> None: + """ + Compose a vspec model into a stand-alone, re-parseable folder snapshot. + + Validates the full model (with expansion) then writes a faithful snapshot + of the raw authored content — so fields such as `instances`, `instantiate`, + `aggregate`, `arraysize`, and any instance-level overrides are preserved + exactly as written. + + The output folder contains: + model_snapshot.vspec — signal tree (always written) + structs_snapshot.vspec — custom struct types (written only when --types is given) + + Both files can be used directly as -s / --types inputs for any subsequent + vspec export invocation. + """ + if output_dir.exists(): + log.info(f"Output directory already exists, writing into it: {output_dir}") + else: + output_dir.mkdir(parents=True) + + # Phase 1: validate the full model with expansion — any real error aborts here. + get_trees( + vspec=vspec, + include_dirs=include_dirs, + aborts=aborts, + strict=strict, + extended_attributes=extended_attributes, + quantities=quantities, + units=units, + types=types, + overlays=overlays, + expand=True, + strict_exceptions_file=strict_exceptions, + ) + + log.info(f"Composing vspec model into {output_dir}...") + + # Phase 2: raw serialization — faithfully preserve exactly what was authored, + # including instance-level overrides and unexpanded instance branches. + include_dirs_list = list(include_dirs) + raw_vspec = load_vspec(include_dirs_list, [vspec] + list(overlays)) + tree_data = {fqn: _filter_raw_node(attrs, SNAPSHOT_EXCLUDE_FIELDS) for fqn, attrs in raw_vspec.data.items()} + model_out = output_dir / MODEL_SNAPSHOT_FILENAME + if model_out.exists(): + log.info(f"Overwriting existing file: {model_out}") + export_yaml(model_out, tree_data) + log.info(f"Signal tree written to {model_out}") + + if types: + raw_types = load_vspec(include_dirs_list, list(types), "Types") + structs_data = {fqn: _filter_raw_node(attrs, SNAPSHOT_EXCLUDE_FIELDS) for fqn, attrs in raw_types.data.items()} + structs_out = output_dir / STRUCTS_SNAPSHOT_FILENAME + if structs_out.exists(): + log.info(f"Overwriting existing file: {structs_out}") + export_yaml(structs_out, structs_data) + log.info(f"Struct types written to {structs_out}") + + # --- units snapshot --- + resolved_units = _resolve_paths(units, vspec.parent / "units.yaml") + if resolved_units: + # dynamic_units contains both key (e.g. "km") and unit-string (e.g. "kilometer") + # aliases pointing to the same object. Keep only canonical key entries. + unit_data = load_units(resolved_units) + units_snapshot = { + k: v.model_dump(by_alias=True, exclude_none=True, exclude={"key"}) for k, v in unit_data.items() + } + units_out = output_dir / UNITS_SNAPSHOT_FILENAME + if units_out.exists(): + log.info(f"Overwriting existing file: {units_out}") + with open(units_out, "w", encoding="utf-8") as f: + yaml.dump(units_snapshot, f, default_flow_style=False, sort_keys=True, allow_unicode=True) + log.info(f"Units written to {units_out}") + + # --- quantities snapshot --- + resolved_quantities = _resolve_paths(quantities, vspec.parent / "quantities.yaml") + if resolved_quantities: + quantity_data = load_quantities(resolved_quantities) + quantities_snapshot = {k: v.model_dump(exclude_none=True) for k, v in quantity_data.items()} + quantities_out = output_dir / QUANTITIES_SNAPSHOT_FILENAME + if quantities_out.exists(): + log.info(f"Overwriting existing file: {quantities_out}") + with open(quantities_out, "w", encoding="utf-8") as f: + yaml.dump(quantities_snapshot, f, default_flow_style=False, sort_keys=True, allow_unicode=True) + log.info(f"Quantities written to {quantities_out}") diff --git a/src/vss_tools/diff.py b/src/vss_tools/diff.py new file mode 100644 index 00000000..23afd166 --- /dev/null +++ b/src/vss_tools/diff.py @@ -0,0 +1,571 @@ +# Copyright (c) 2026 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0 which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +""" +Pure diff logic for vspec compose snapshot folders. + +Compares two snapshot folders produced by `vspec compose` and returns a +structured list of ADDED, REMOVED, and MODIFIED change events covering: + - model (model_snapshot.vspec) + - structs (structs_snapshot.vspec) + - units (units_snapshot.yaml) + - quantities (quantities_snapshot.yaml) + +No vss-tools tree parsing is performed — snapshots are already flat dicts. +""" + +from __future__ import annotations + +import itertools +from pathlib import Path +from typing import Any + +import yaml + +from vss_tools import log +from vss_tools.compose import ( + MODEL_SNAPSHOT_FILENAME, + QUANTITIES_SNAPSHOT_FILENAME, + STRUCTS_SNAPSHOT_FILENAME, + UNITS_SNAPSHOT_FILENAME, +) +from vss_tools.tree import expand_string + +# --------------------------------------------------------------------------- +# Change type constants +# --------------------------------------------------------------------------- +ADDED = "ADDED" +REMOVED = "REMOVED" +MODIFIED = "MODIFIED" + +# --------------------------------------------------------------------------- +# modl IR kind constants +# --------------------------------------------------------------------------- +ENTITY = "ENTITY" +PROPERTY = "PROPERTY" +ENUMERATION_SET = "ENUMERATION_SET" +ENUM_VALUE = "ENUM_VALUE" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def load_flat_yaml(path: Path) -> dict[str, Any]: + """Load a flat snapshot YAML file. Returns empty dict when file is absent.""" + if not path.exists(): + return {} + content = yaml.safe_load(path.read_text(encoding="utf-8")) + return content if isinstance(content, dict) else {} + + +def _node_type(attrs: dict[str, Any]) -> str: + """Extract the VSS node type label from an attribute dict.""" + return str(attrs.get("type", "unknown")) + + +def _field_diff(prev_attrs: dict[str, Any], curr_attrs: dict[str, Any]) -> list[dict[str, Any]]: + """ + Return a list of per-attribute change dicts for fields that differ. + Includes fields that appeared or disappeared (previous/current = None). + """ + all_keys = set(prev_attrs) | set(curr_attrs) + changes = [] + for k in sorted(all_keys): + pv = prev_attrs.get(k) + cv = curr_attrs.get(k) + if pv != cv: + changes.append({"attribute": k, "previous": pv, "current": cv}) + return changes + + +# --------------------------------------------------------------------------- +# modl IR translation helpers +# --------------------------------------------------------------------------- + + +def _vss_kind(node_type: str, source: str) -> str: + """Map a VSS node type + source to a modl IR element kind.""" + if source == "quantities": + return ENUMERATION_SET + if source == "units": + return ENUM_VALUE + # model or structs + if node_type in ("branch", "struct"): + return ENTITY + return PROPERTY + + +def _extract_datatype(value: str | None) -> tuple[str | None, bool]: + """Split a VSS datatype string into base type and is_list flag.""" + if value is None: + return None, False + if value.endswith("[]"): + return value[:-2], True + return value, False + + +def _expand_instances(value: Any) -> list[str]: + """ + Expand a VSS instances value into fully-qualified instance path labels. + + Each top-level list item is one dimension; the result is the Cartesian + product of all dimensions joined with '.'. Examples: + + "Row[1,4]" → ["Row1","Row2","Row3","Row4"] + ["Left", "Right"] → ["Left", "Right"] + ["Row[1,2]", ["DriverSide","PassengerSide"]] → ["Row1.DriverSide", + "Row1.PassengerSide", + "Row2.DriverSide", + "Row2.PassengerSide"] + """ + if value is None: + return [] + if isinstance(value, str): + return expand_string(value) + if not isinstance(value, list): + return [str(value)] + + # If every item is a plain string with no range syntax the whole list is + # a single dimension (e.g. ["Left", "Right"]). + if all(isinstance(item, str) and "[" not in item for item in value): + return [str(item) for item in value] + + # Otherwise each item is its own dimension; compute Cartesian product. + dimensions: list[list[str]] = [] + for item in value: + if isinstance(item, list): + dimensions.append([str(x) for x in item]) + elif isinstance(item, str) and "[" in item: + dimensions.append(expand_string(item)) + else: + dimensions.append([str(item)]) + + if not dimensions: + return [] + return [".".join(combo) for combo in itertools.product(*dimensions)] + + +def _expand_instance_fqns(base_fqn: str, instances_value: Any) -> list[str]: + """ + Expand a branch's instances into all branch FQN paths produced by expansion, + mirroring expand_instance() in tree.py — including every intermediate dimension hop. + + Each top-level entry in instances_value is one dimension, iterated exactly as + expand_instances() iterates ``for instance in node.data.instances``. + Roots advance (next dimension attaches to generated nodes) when the dimension + entry is a list or expands to more than one name — matching the tree.py rule: + ``len(names) > 1 or isinstance(instance, list) → return nodes, nodes``. + """ + if not instances_value: + return [] + + # Normalise: if instances_value itself is not a list, wrap it so we can + # iterate uniformly (mirrors tree.py where data.instances is always iterable). + raw_dimensions: list[Any] = instances_value if isinstance(instances_value, list) else [instances_value] + + roots: list[str] = [base_fqn] + all_created: list[str] = [] + + for dimension in raw_dimensions: + if isinstance(dimension, list): + # Inner list: each element is an explicit name (possibly range syntax) + names: list[str] = [] + for item in dimension: + s = str(item) + names.extend(expand_string(s) if "[" in s else [s]) + advance = True # isinstance(instance, list) → return nodes, nodes + elif isinstance(dimension, str) and "[" in dimension: + names = expand_string(dimension) + advance = len(names) > 1 # range expansion → return nodes, nodes + else: + names = [str(dimension)] + advance = False # single plain string → return roots, nodes + + new_nodes: list[str] = [] + for root in roots: + for name in names: + fqn = f"{root}.{name}" + all_created.append(fqn) + new_nodes.append(fqn) + + if advance: + roots = new_nodes + + return all_created + + +def _inject_instance_expansions(raw_events: list[dict[str, Any]]) -> list[dict[str, Any]]: + """ + For every ENTITY ADDED or REMOVED event that carries an ``instances`` attribute, + inject ADDED or REMOVED events for all branch FQNs produced by instance expansion. + + This mirrors the YAML exporter's behaviour: instances expand into concrete branch + hops (one per dimension level), so those branches must appear in the diff just as + they would appear in any expanded output. Without these synthetic events, + ``_inject_entity_content`` has no parent to anchor children to and creates phantom + ENTITY MODIFIED events instead. + """ + result: list[dict[str, Any]] = [] + for ev in raw_events: + result.append(ev) + if ev["type"] not in (ADDED, REMOVED): + continue + if _vss_kind(ev["node_type"], ev["source"]) != ENTITY: + continue + attrs = ev.get("attributes") or {} + instances_value = attrs.get("instances") + if not instances_value: + continue + + description = attrs.get("description", "") + for fqn in _expand_instance_fqns(ev["path"], instances_value): + result.append( + { + "type": ev["type"], + "source": ev["source"], + "path": fqn, + "node_type": "branch", + "attributes": { + "type": "branch", + "description": description, + }, + } + ) + return result + + +def _map_aspects_added(attrs: dict[str, Any], source: str, node_type: str) -> dict[str, Any]: + """Build the full aspects snapshot for an ADDED event.""" + aspects = {k: v for k, v in attrs.items() if k not in ("fka", "quantity")} + kind = _vss_kind(node_type, source) + if kind == PROPERTY: + raw_dt = aspects.pop("datatype", None) + output_type, is_list = _extract_datatype(raw_dt) + if output_type is not None: + aspects["output_type"] = output_type + aspects["is_list"] = is_list + aspects.setdefault("is_required", False) + elif kind == ENTITY: + if "instances" in aspects: + aspects["instances"] = _expand_instances(aspects["instances"]) + elif kind == ENUM_VALUE: + # rename the 'unit' display-name key to 'symbol' + if "unit" in aspects: + aspects["symbol"] = aspects.pop("unit") + return aspects + + +def _map_aspects_delta(attribute_changes: list[dict[str, Any]]) -> dict[str, Any]: + """Build the changed-keys-only aspects dict for a MODIFIED event.""" + delta: dict[str, Any] = {} + for ac in attribute_changes: + attr, current = ac["attribute"], ac["current"] + if attr == "fka": + continue + if attr == "datatype": + output_type, is_list = _extract_datatype(current) + if output_type is not None: + delta["output_type"] = output_type + delta["is_list"] = is_list + elif attr == "instances": + delta["instances"] = _expand_instances(current) + else: + delta[attr] = current + return delta + + +def _parent_label(path: str, source: str, attrs: dict[str, Any]) -> str | None: + """Derive the modl parent_label for an event.""" + if source == "quantities": + return None + if source == "units": + qty = attrs.get("quantity") + if not qty: + log.warning(f"Unit '{path}' has no quantity — skipping parent_label assignment") + return None + return str(qty) + # model or structs: parent is the FQN prefix + if "." in path: + return path.rsplit(".", 1)[0] + return None + + +def _to_modl_events(raw_events: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Translate internal diff events to the modl adapter IR format.""" + result = [] + for ev in raw_events: + source = ev["source"] + node_type = ev["node_type"] + path = ev["path"] + change_type = ev["type"] + attrs = ev.get("attributes") or {} + + kind = _vss_kind(node_type, source) + parent = _parent_label(path, source, attrs) + + event: dict[str, Any] = { + "label": path, + "kind": kind, + "change_type": change_type, + } + if parent is not None: + event["parent_label"] = parent + + if change_type == ADDED: + event["aspects"] = _map_aspects_added(attrs, source, node_type) + elif change_type == REMOVED: + pass # modl: REMOVED must carry no aspects + else: # MODIFIED + if ev.get("previous_path"): + event["renamed_from"] = ev["previous_path"] + event["aspects"] = _map_aspects_delta(ev.get("attribute_changes") or []) + + result.append(event) + return result + + +def _inject_entity_content(modl_events: list[dict[str, Any]]) -> list[dict[str, Any]]: + """ + Ensure every ENTITY/ENUMERATION_SET has a MODIFIED event with a content list + summarising which children changed, when those children are ADDED/REMOVED/MODIFIED + but the parent itself was not independently changed. + """ + entity_kinds = {ENTITY, ENUMERATION_SET} + child_kinds = {PROPERTY, ENUM_VALUE} + + # Track which entity labels were fully ADDED or REMOVED (no content injection needed) + entity_added_removed: set[str] = set() + # Map entity label → index in the working list for already-present MODIFIED entities + entity_modified: dict[str, int] = {} + + events = list(modl_events) + + for i, ev in enumerate(events): + if ev["kind"] in entity_kinds: + if ev["change_type"] in (ADDED, REMOVED): + entity_added_removed.add(ev["label"]) + elif ev["change_type"] == MODIFIED: + entity_modified[ev["label"]] = i + + for ev in modl_events: + if ev["kind"] not in child_kinds: + continue + parent = ev.get("parent_label") + if parent is None or parent in entity_added_removed: + continue + + content_item = {"label": ev["label"], "change_type": ev["change_type"]} + + if parent in entity_modified: + events[entity_modified[parent]].setdefault("content", []).append(content_item) + else: + new_entity_event: dict[str, Any] = { + "label": parent, + "kind": ENTITY, + "change_type": MODIFIED, + "aspects": {}, + "content": [content_item], + } + entity_modified[parent] = len(events) + events.append(new_entity_event) + + return events + + +# --------------------------------------------------------------------------- +# Core diff +# --------------------------------------------------------------------------- + + +def _diff_dicts( + previous: dict[str, Any], + current: dict[str, Any], + source: str, + detect_renames: bool = True, +) -> list[dict[str, Any]]: + """ + Compute the full diff between two flat dicts. + + When detect_renames=True (model/structs) rename detection runs: + 1. Explicit: added node has fka entry matching a removed path AND node_type matches. + 2. Cascade: confirmed parent renames propagate to children by FQN prefix substitution; + each cascaded child is independently checked for attribute changes too. + """ + prev_keys = set(previous) + curr_keys = set(current) + + raw_added = {k: current[k] for k in curr_keys - prev_keys} + raw_removed = {k: previous[k] for k in prev_keys - curr_keys} + + events: list[dict[str, Any]] = [] + + # --- rename detection --- + consumed_added: set[str] = set() + consumed_removed: set[str] = set() + confirmed_renames: list[tuple[str, str]] = [] # (old_prefix, new_prefix) + + if detect_renames: + # Pass 1 — explicit fka + for new_path, new_attrs in raw_added.items(): + fka: list[str] = new_attrs.get("fka") or [] + if isinstance(fka, str): + fka = [fka] + new_nt = _node_type(new_attrs) + for old_path in fka: + if old_path in raw_removed and old_path not in consumed_removed: + old_attrs = raw_removed[old_path] + # node_type must match to confirm rename (not just any fka hit) + if _node_type(old_attrs) != new_nt: + continue + attr_changes = _field_diff(old_attrs, new_attrs) + # Remove fka from attribute_changes — it's expected to differ + attr_changes = [c for c in attr_changes if c["attribute"] != "fka"] + events.append( + { + "type": MODIFIED, + "source": source, + "path": new_path, + "previous_path": old_path, + "node_type": new_nt, + "cascade": False, + "attribute_changes": attr_changes, + } + ) + consumed_added.add(new_path) + consumed_removed.add(old_path) + confirmed_renames.append((old_path, new_path)) + break + + # Pass 2 — cascade: children whose FQN prefix changed due to a confirmed parent rename + for old_prefix, new_prefix in confirmed_renames: + old_child_prefix = old_prefix + "." + new_child_prefix = new_prefix + "." + + for new_path, new_attrs in raw_added.items(): + if new_path in consumed_added: + continue + if not new_path.startswith(new_child_prefix): + continue + # Reconstruct the expected old path + old_path = old_child_prefix + new_path[len(new_child_prefix) :] + if old_path not in raw_removed or old_path in consumed_removed: + continue + old_attrs = raw_removed[old_path] + if _node_type(old_attrs) != _node_type(new_attrs): + continue + attr_changes = _field_diff(old_attrs, new_attrs) + new_nt = _node_type(new_attrs) + events.append( + { + "type": MODIFIED, + "source": source, + "path": new_path, + "previous_path": old_path, + "node_type": new_nt, + "cascade": True, + "attribute_changes": attr_changes, + } + ) + consumed_added.add(new_path) + consumed_removed.add(old_path) + + # --- plain ADDED --- + for path, attrs in sorted(raw_added.items()): + if path in consumed_added: + continue + nt = _node_type(attrs) + events.append( + { + "type": ADDED, + "source": source, + "path": path, + "node_type": nt, + "attributes": attrs, + } + ) + + # --- plain REMOVED --- + for path, attrs in sorted(raw_removed.items()): + if path in consumed_removed: + continue + nt = _node_type(attrs) + events.append( + { + "type": REMOVED, + "source": source, + "path": path, + "node_type": nt, + "attributes": attrs, + } + ) + + # --- MODIFIED (attribute changes on nodes present in both) --- + for path in sorted(prev_keys & curr_keys): + attr_changes = _field_diff(previous[path], current[path]) + if not attr_changes: + continue + nt = _node_type(current[path]) + events.append( + { + "type": MODIFIED, + "source": source, + "path": path, + "node_type": nt, + "attribute_changes": attr_changes, + } + ) + + return events + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def diff_folders(previous_dir: Path | None, current_dir: Path) -> dict[str, Any]: + """ + Diff two compose snapshot folders and return a modl-compatible diff report. + + Returns a dict with: + previous, current — folder paths as strings + changes — ordered list of modl IR change events + + When previous_dir is None (first-run mode), every element in current_dir + is treated as ADDED and emitted with its complete aspects snapshot. + """ + raw_changes: list[dict[str, Any]] = [] + + sources = [ + (MODEL_SNAPSHOT_FILENAME, "model", True), + (STRUCTS_SNAPSHOT_FILENAME, "structs", True), + (UNITS_SNAPSHOT_FILENAME, "units", False), + (QUANTITIES_SNAPSHOT_FILENAME, "quantities", False), + ] + + for filename, source_label, detect_renames in sources: + prev = load_flat_yaml(previous_dir / filename) if previous_dir else {} + curr = load_flat_yaml(current_dir / filename) + if not prev and not curr: + continue + raw_changes.extend(_diff_dicts(prev, curr, source_label, detect_renames=detect_renames)) + + raw_changes = _inject_instance_expansions(raw_changes) + modl_events = _to_modl_events(raw_changes) + modl_events = _inject_entity_content(modl_events) + + _KIND_ORDER = {ENTITY: 0, ENUMERATION_SET: 1, PROPERTY: 2, ENUM_VALUE: 3} + modl_events.sort(key=lambda e: _KIND_ORDER.get(e["kind"], 99)) + + return { + "previous": str(previous_dir) if previous_dir else None, + "current": str(current_dir), + "changes": modl_events, + } diff --git a/src/vss_tools/diff_cmd.py b/src/vss_tools/diff_cmd.py new file mode 100644 index 00000000..a87de51f --- /dev/null +++ b/src/vss_tools/diff_cmd.py @@ -0,0 +1,65 @@ +# Copyright (c) 2026 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0 which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +import json +import sys +from pathlib import Path + +import rich_click as click + +from vss_tools import log +from vss_tools.diff import diff_folders + + +@click.command() +@click.option( + "--previous", + "-p", + default=None, + type=click.Path(exists=True, file_okay=False, readable=True, path_type=Path), + help="Composed model snapshot used as the basis for the comparison." + "Omit for first-run mode (all elements treated as ADDED).", +) +@click.option( + "--current", + "-c", + required=True, + type=click.Path(exists=True, file_okay=False, readable=True, path_type=Path), + help="Composed model representing the current version.", +) +@click.option( + "--output", + "-o", + type=click.Path(dir_okay=False, writable=True, path_type=Path), + help="Write JSON output to this file instead of stdout.", +) +def cli(previous: Path | None, current: Path, output: Path | None) -> None: + """ + Diff two vspec compose snapshot folders and produce a modl-compatible diff report. + + Compares signals, structs, units, and quantities across two snapshot + folders produced by `vspec compose` and reports ADDED, REMOVED, and + MODIFIED changes as a structured JSON document in the modl adapter IR format. + + When --previous is omitted (first-run mode), every element in --current + is treated as ADDED and emitted with its complete aspects snapshot. + + Output goes to stdout by default; use --output to write to a file. + """ + if previous: + log.info(f"Diffing snapshots: {previous} → {current}") + else: + log.info(f"First-run mode: treating all elements in {current} as ADDED") + result = diff_folders(previous, current) + out_text = json.dumps(result, indent=2, ensure_ascii=False) + + if output: + output.write_text(out_text, encoding="utf-8") + log.info(f"Diff written to {output}") + else: + sys.stdout.write(out_text + "\n") diff --git a/src/vss_tools/model.py b/src/vss_tools/model.py index 362a191c..cf62ac3c 100644 --- a/src/vss_tools/model.py +++ b/src/vss_tools/model.py @@ -89,6 +89,7 @@ def as_dict( with_extra_attributes: bool = True, exclude_fields: list[str] = EXPORT_EXCLUDE_ATTRIBUTES, extended_attributes: tuple[str, ...] = (), + exclude_defaults: bool = False, ) -> dict[str, Any]: excludes = exclude_fields.copy() if not with_extra_attributes: @@ -96,7 +97,13 @@ def as_dict( if extra_attribute not in extended_attributes: excludes.append(extra_attribute) data = {} - for k, v in self.model_dump(mode="json", exclude_none=False, exclude=set(excludes)).items(): + dump = self.model_dump( + mode="json", + exclude_none=False, + exclude=set(excludes), + exclude_defaults=exclude_defaults, + ) + for k, v in dump.items(): if k not in extended_attributes: if v == []: continue diff --git a/src/vss_tools/tree.py b/src/vss_tools/tree.py index 10b43044..10f6b74f 100644 --- a/src/vss_tools/tree.py +++ b/src/vss_tools/tree.py @@ -17,6 +17,7 @@ from vss_tools import log from vss_tools.datatypes import Datatypes, dynamic_datatypes, dynamic_struct_schemas, is_array from vss_tools.model import ( + EXPORT_EXCLUDE_ATTRIBUTES, ModelValidationException, VSSData, VSSDataBranch, @@ -351,7 +352,13 @@ def get_default_first_allowed_violations(self) -> list[list[str]]: log.info(f"default != allowed[0] violations (before applying exceptions): {len(violations)}") return violations - def as_flat_dict(self, with_extra_attributes: bool, extended_attributes: tuple[str, ...] = ()) -> dict[str, Any]: + def as_flat_dict( + self, + with_extra_attributes: bool, + extended_attributes: tuple[str, ...] = (), + exclude_fields: list[str] = EXPORT_EXCLUDE_ATTRIBUTES, + exclude_defaults: bool = False, + ) -> dict[str, Any]: """ Generates a flat dict and whether to include user attributes or not @@ -360,7 +367,12 @@ def as_flat_dict(self, with_extra_attributes: bool, extended_attributes: tuple[s node: VSSNode for node in PreOrderIter(self): key = node.get_fqn() - data[key] = node.data.as_dict(with_extra_attributes, extended_attributes=extended_attributes) + data[key] = node.data.as_dict( + with_extra_attributes, + exclude_fields=exclude_fields, + extended_attributes=extended_attributes, + exclude_defaults=exclude_defaults, + ) return data def get_instance_root(self, depth: int = 0) -> tuple[VSSNode, int]: diff --git a/tests/vspec/test_compose/expected/model_snapshot.vspec b/tests/vspec/test_compose/expected/model_snapshot.vspec new file mode 100644 index 00000000..fe3a2c53 --- /dev/null +++ b/tests/vspec/test_compose/expected/model_snapshot.vspec @@ -0,0 +1,28 @@ +A: + description: Branch A. + type: branch + +A.Codes: + arraysize: 4 + datatype: uint8[] + description: Error codes. + type: sensor + +A.Door: + description: Door branch with instances. + instances: + - Row[1,2] + - - Left + - Right + type: branch + +A.Door.Config: + datatype: Types.Point + description: Door config that should not be instantiated. + instantiate: false + type: sensor + +A.Door.IsOpen: + datatype: boolean + description: Is the door open? + type: actuator diff --git a/tests/vspec/test_compose/expected/model_snapshot_no_types.vspec b/tests/vspec/test_compose/expected/model_snapshot_no_types.vspec new file mode 100644 index 00000000..96b88f5c --- /dev/null +++ b/tests/vspec/test_compose/expected/model_snapshot_no_types.vspec @@ -0,0 +1,22 @@ +A: + description: Branch A. + type: branch + +A.Codes: + arraysize: 4 + datatype: uint8[] + description: Error codes. + type: sensor + +A.Door: + description: Door branch with instances. + instances: + - Row[1,2] + - - Left + - Right + type: branch + +A.Door.IsOpen: + datatype: boolean + description: Is the door open? + type: actuator diff --git a/tests/vspec/test_compose/expected/structs_snapshot.vspec b/tests/vspec/test_compose/expected/structs_snapshot.vspec new file mode 100644 index 00000000..55bb762e --- /dev/null +++ b/tests/vspec/test_compose/expected/structs_snapshot.vspec @@ -0,0 +1,17 @@ +Types: + description: Custom data types. + type: branch + +Types.Point: + description: A 2D point struct. + type: struct + +Types.Point.X: + datatype: float + description: X coordinate. + type: property + +Types.Point.Y: + datatype: float + description: Y coordinate. + type: property diff --git a/tests/vspec/test_compose/test.vspec b/tests/vspec/test_compose/test.vspec new file mode 100644 index 00000000..40406819 --- /dev/null +++ b/tests/vspec/test_compose/test.vspec @@ -0,0 +1,28 @@ +# +A: + type: branch + description: Branch A. + +A.Door: + type: branch + instances: + - Row[1,2] + - ["Left", "Right"] + description: Door branch with instances. + +A.Door.IsOpen: + datatype: boolean + type: actuator + description: Is the door open? + +A.Door.Config: + datatype: Types.Point + type: sensor + instantiate: false + description: Door config that should not be instantiated. + +A.Codes: + datatype: uint8[] + type: sensor + arraysize: 4 + description: Error codes. diff --git a/tests/vspec/test_compose/test_compose.py b/tests/vspec/test_compose/test_compose.py new file mode 100644 index 00000000..558616a4 --- /dev/null +++ b/tests/vspec/test_compose/test_compose.py @@ -0,0 +1,96 @@ +# Copyright (c) 2026 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0 which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +import filecmp +import subprocess +from pathlib import Path + +HERE = Path(__file__).resolve().parent +TEST_UNITS = HERE / ".." / "test_units.yaml" +TEST_QUANT = HERE / ".." / "test_quantities.yaml" +EXPECTED = HERE / "expected" + + +def _compose(tmp_path: Path, vspec: Path, types: Path | None = None) -> tuple[Path, subprocess.CompletedProcess]: + out = tmp_path / "out" + cmd = [ + "vspec", + "compose", + "-u", + str(TEST_UNITS), + "-q", + str(TEST_QUANT), + "-s", + str(vspec), + "-o", + str(out), + ] + if types: + cmd += ["--types", str(types)] + result = subprocess.run(cmd, capture_output=True, text=True) + return out, result + + +def test_compose_with_structs(tmp_path): + """Both output files are written and match expected fixtures.""" + out, result = _compose(tmp_path, HERE / "test.vspec", HERE / "types.vspec") + assert result.returncode == 0, result.stderr + + assert filecmp.cmp(out / "model_snapshot.vspec", EXPECTED / "model_snapshot.vspec") + assert filecmp.cmp(out / "structs_snapshot.vspec", EXPECTED / "structs_snapshot.vspec") + + +def test_compose_no_types(tmp_path): + """Without --types only model_snapshot.vspec is written; structs file absent.""" + out, result = _compose(tmp_path, HERE / "test_no_types.vspec") + assert result.returncode == 0, result.stderr + + assert filecmp.cmp(out / "model_snapshot.vspec", EXPECTED / "model_snapshot_no_types.vspec") + assert not (out / "structs_snapshot.vspec").exists() + + +def test_compose_round_trip(tmp_path): + """Compose output is fully self-contained: re-parseable using only snapshot files.""" + out, result = _compose(tmp_path, HERE / "test.vspec", HERE / "types.vspec") + assert result.returncode == 0, result.stderr + + yaml_out = tmp_path / "round_trip.yaml" + rt = subprocess.run( + [ + "vspec", + "export", + "yaml", + "-u", + str(out / "units_snapshot.yaml"), + "-q", + str(out / "quantities_snapshot.yaml"), + "-s", + str(out / "model_snapshot.vspec"), + "--types", + str(out / "structs_snapshot.vspec"), + "--output", + str(yaml_out), + ], + capture_output=True, + text=True, + ) + assert rt.returncode == 0, rt.stderr + assert yaml_out.exists() + + +def test_compose_existing_dir_is_reused(tmp_path): + """Pre-existing output directory is reused and stale files are overwritten.""" + out = tmp_path / "out" + out.mkdir() + stale = out / "model_snapshot.vspec" + stale.write_text("stale content", encoding="utf-8") + + _, result = _compose(tmp_path, HERE / "test_no_types.vspec") + assert result.returncode == 0, result.stderr + + assert stale.read_text(encoding="utf-8") != "stale content" diff --git a/tests/vspec/test_compose/test_no_types.vspec b/tests/vspec/test_compose/test_no_types.vspec new file mode 100644 index 00000000..4bac8561 --- /dev/null +++ b/tests/vspec/test_compose/test_no_types.vspec @@ -0,0 +1,22 @@ +# +A: + type: branch + description: Branch A. + +A.Door: + type: branch + instances: + - Row[1,2] + - ["Left", "Right"] + description: Door branch with instances. + +A.Door.IsOpen: + datatype: boolean + type: actuator + description: Is the door open? + +A.Codes: + datatype: uint8[] + type: sensor + arraysize: 4 + description: Error codes. diff --git a/tests/vspec/test_compose/types.vspec b/tests/vspec/test_compose/types.vspec new file mode 100644 index 00000000..63569303 --- /dev/null +++ b/tests/vspec/test_compose/types.vspec @@ -0,0 +1,17 @@ +Types: + type: branch + description: Custom data types. + +Types.Point: + type: struct + description: A 2D point struct. + +Types.Point.X: + type: property + datatype: float + description: X coordinate. + +Types.Point.Y: + type: property + datatype: float + description: Y coordinate. diff --git a/tests/vspec/test_diff/fixtures/current_snapshot/model_snapshot.vspec b/tests/vspec/test_diff/fixtures/current_snapshot/model_snapshot.vspec new file mode 100644 index 00000000..964f2867 --- /dev/null +++ b/tests/vspec/test_diff/fixtures/current_snapshot/model_snapshot.vspec @@ -0,0 +1,31 @@ +# Current snapshot: model_snapshot.vspec +# A.Door renamed to A.Portal (fka: A.Door) +# A.Portal.IsOpen cascaded rename (datatype changed too) +# A.Portal.LockState removed +# A.Humidity added +# A.Speed unit changed +A: + description: Root branch. + type: branch + +A.Humidity: + datatype: float + description: Relative humidity. + type: sensor + unit: percent + +A.Portal: + description: Door branch. + fka: A.Door + type: branch + +A.Portal.IsOpen: + datatype: string + description: Is the door open? + type: actuator + +A.Speed: + datatype: float + description: Vehicle speed. + type: sensor + unit: m/s diff --git a/tests/vspec/test_diff/fixtures/current_snapshot/units_snapshot.yaml b/tests/vspec/test_diff/fixtures/current_snapshot/units_snapshot.yaml new file mode 100644 index 00000000..54fdbf59 --- /dev/null +++ b/tests/vspec/test_diff/fixtures/current_snapshot/units_snapshot.yaml @@ -0,0 +1,14 @@ +# Units snapshot — current (km/h removed, percent added) +m/s: + allowed-datatypes: + - float + definition: Metres per second + quantity: speed + unit: m/s + +percent: + allowed-datatypes: + - float + definition: Percentage + quantity: relation + unit: percent diff --git a/tests/vspec/test_diff/fixtures/no_changes_snapshot/model_snapshot.vspec b/tests/vspec/test_diff/fixtures/no_changes_snapshot/model_snapshot.vspec new file mode 100644 index 00000000..74df3cda --- /dev/null +++ b/tests/vspec/test_diff/fixtures/no_changes_snapshot/model_snapshot.vspec @@ -0,0 +1,10 @@ +# No-change snapshot: identical model +A: + description: Root branch. + type: branch + +A.Speed: + datatype: float + description: Vehicle speed. + type: sensor + unit: km/h diff --git a/tests/vspec/test_diff/fixtures/previous_snapshot/model_snapshot.vspec b/tests/vspec/test_diff/fixtures/previous_snapshot/model_snapshot.vspec new file mode 100644 index 00000000..52f02891 --- /dev/null +++ b/tests/vspec/test_diff/fixtures/previous_snapshot/model_snapshot.vspec @@ -0,0 +1,25 @@ +# Previous snapshot: model_snapshot.vspec +# Branch A.Door with signals; will be renamed to A.Portal in current snapshot. +A: + description: Root branch. + type: branch + +A.Door: + description: Door branch. + type: branch + +A.Door.IsOpen: + datatype: boolean + description: Is the door open? + type: actuator + +A.Door.LockState: + datatype: string + description: Lock state of the door. + type: sensor + +A.Speed: + datatype: float + description: Vehicle speed. + type: sensor + unit: km/h diff --git a/tests/vspec/test_diff/fixtures/previous_snapshot/units_snapshot.yaml b/tests/vspec/test_diff/fixtures/previous_snapshot/units_snapshot.yaml new file mode 100644 index 00000000..b965c06c --- /dev/null +++ b/tests/vspec/test_diff/fixtures/previous_snapshot/units_snapshot.yaml @@ -0,0 +1,14 @@ +# Units snapshot — previous +km/h: + allowed-datatypes: + - float + definition: Kilometres per hour + quantity: speed + unit: km/h + +m/s: + allowed-datatypes: + - float + definition: Metres per second + quantity: speed + unit: m/s diff --git a/tests/vspec/test_diff/test_diff.py b/tests/vspec/test_diff/test_diff.py new file mode 100644 index 00000000..949902ef --- /dev/null +++ b/tests/vspec/test_diff/test_diff.py @@ -0,0 +1,464 @@ +# Copyright (c) 2026 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0 which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +"""Tests for vss_tools.diff (pure logic) and `vspec diff` CLI.""" + +import json +from pathlib import Path +from typing import Any + +import pytest +from click.testing import CliRunner +from vss_tools.diff import ( + ADDED, + ENTITY, + ENUM_VALUE, + MODIFIED, + PROPERTY, + REMOVED, + diff_folders, + load_flat_yaml, +) +from vss_tools.diff_cmd import cli + +FIXTURES = Path(__file__).parent / "fixtures" +PREVIOUS = FIXTURES / "previous_snapshot" +CURRENT = FIXTURES / "current_snapshot" +NO_CHANGES = FIXTURES / "no_changes_snapshot" +EMPTY = FIXTURES / "empty_previous" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def changes_by_label(result: dict[str, Any]) -> dict[str, Any]: + """Index the changes list by 'label' for easy lookup in assertions.""" + return {c["label"]: c for c in result["changes"]} + + +# --------------------------------------------------------------------------- +# load_flat_yaml +# --------------------------------------------------------------------------- + + +class TestLoadFlatYaml: + def test_loads_valid_yaml(self, tmp_path: Path): + f = tmp_path / "snap.vspec" + f.write_text("A:\n type: branch\n description: x\n") + result = load_flat_yaml(f) + assert result == {"A": {"type": "branch", "description": "x"}} + + def test_returns_empty_dict_for_missing_file(self, tmp_path: Path): + assert load_flat_yaml(tmp_path / "nonexistent.yaml") == {} + + def test_returns_empty_dict_for_non_mapping(self, tmp_path: Path): + f = tmp_path / "bad.yaml" + f.write_text("- item1\n- item2\n") + assert load_flat_yaml(f) == {} + + +# --------------------------------------------------------------------------- +# diff_folders — model changes +# --------------------------------------------------------------------------- + + +class TestSignalChanges: + """Covers the main model diff between previous_snapshot and current_snapshot.""" + + @pytest.fixture(scope="class") + def result(self): + return diff_folders(PREVIOUS, CURRENT) + + def test_metadata(self, result: dict[str, Any]): + assert str(PREVIOUS) in result["previous"] + assert str(CURRENT) in result["current"] + + def test_rename_detected(self, result: dict[str, Any]): + by_label = changes_by_label(result) + rename = by_label.get("A.Portal") + assert rename is not None, "A.Portal rename not detected" + assert rename["change_type"] == MODIFIED + assert rename["kind"] == ENTITY + assert rename["renamed_from"] == "A.Door" + + def test_cascade_rename_detected(self, result: dict[str, Any]): + by_label = changes_by_label(result) + cascade = by_label.get("A.Portal.IsOpen") + assert cascade is not None, "A.Portal.IsOpen cascade not detected" + assert cascade["change_type"] == MODIFIED + assert cascade["kind"] == PROPERTY + assert cascade["renamed_from"] == "A.Door.IsOpen" + assert cascade["parent_label"] == "A.Portal" + + def test_cascade_rename_includes_aspect_change(self, result: dict[str, Any]): + by_label = changes_by_label(result) + cascade = by_label["A.Portal.IsOpen"] + # datatype changed from boolean → string, mapped to output_type + assert cascade["aspects"].get("output_type") == "string" + + def test_removed_signal(self, result: dict[str, Any]): + by_label = changes_by_label(result) + removed = by_label.get("A.Door.LockState") + assert removed is not None, "A.Door.LockState should be removed" + assert removed["change_type"] == REMOVED + assert removed["kind"] == PROPERTY + + def test_added_signal(self, result: dict[str, Any]): + by_label = changes_by_label(result) + added = by_label.get("A.Humidity") + assert added is not None + assert added["change_type"] == ADDED + assert added["kind"] == PROPERTY + assert added["parent_label"] == "A" + assert added["aspects"]["output_type"] == "float" + assert added["aspects"]["is_list"] is False + assert added["aspects"]["is_required"] is False + + def test_modified_attribute(self, result: dict[str, Any]): + by_label = changes_by_label(result) + modified = by_label.get("A.Speed") + assert modified is not None, "A.Speed unit change not detected" + assert modified["change_type"] == MODIFIED + assert modified["aspects"].get("unit") == "m/s" + + def test_no_summary_field(self, result: dict[str, Any]): + assert "summary" not in result + + def test_no_message_field_on_any_change(self, result: dict[str, Any]): + for c in result["changes"]: + assert "message" not in c, f"Unexpected 'message' on change: {c}" + + def test_no_cascade_field_on_any_change(self, result: dict[str, Any]): + for c in result["changes"]: + assert "cascade" not in c, f"Unexpected 'cascade' on change: {c}" + + +# --------------------------------------------------------------------------- +# diff_folders — units changes +# --------------------------------------------------------------------------- + + +class TestUnitChanges: + @pytest.fixture(scope="class") + def result(self): + return diff_folders(PREVIOUS, CURRENT) + + def test_unit_removed(self, result: dict[str, Any]): + by_label = changes_by_label(result) + removed = by_label.get("km/h") + assert removed is not None + assert removed["change_type"] == REMOVED + assert removed["kind"] == ENUM_VALUE + assert removed["parent_label"] == "speed" + + def test_unit_added(self, result: dict[str, Any]): + by_label = changes_by_label(result) + added = by_label.get("percent") + assert added is not None + assert added["change_type"] == ADDED + assert added["kind"] == ENUM_VALUE + assert added["parent_label"] == "relation" + # 'unit' renamed to 'symbol' in aspects + assert "symbol" in added["aspects"] + assert "unit" not in added["aspects"] + + def test_unit_has_no_output_type(self, result: dict[str, Any]): + by_label = changes_by_label(result) + added = by_label.get("percent") + assert added is not None + assert "output_type" not in added["aspects"] + + +# --------------------------------------------------------------------------- +# diff_folders — entity content injection +# --------------------------------------------------------------------------- + + +class TestEntityContent: + @pytest.fixture(scope="class") + def result(self): + return diff_folders(PREVIOUS, CURRENT) + + def test_parent_entity_has_content_for_added_child(self, result: dict[str, Any]): + # A.Humidity was added under A — A should appear as ENTITY MODIFIED with content + by_label = changes_by_label(result) + entity_a = by_label.get("A") + assert entity_a is not None, "Synthetic ENTITY MODIFIED for 'A' not found" + assert entity_a["kind"] == ENTITY + assert entity_a["change_type"] == MODIFIED + content_labels = [item["label"] for item in entity_a.get("content", [])] + assert "A.Humidity" in content_labels + + def test_parent_entity_has_content_for_removed_child(self, result: dict[str, Any]): + # A.Door.LockState was removed — its parent A.Door (now A.Portal) should have content + # A.Portal is already MODIFIED (rename) so content is injected into it + by_label = changes_by_label(result) + portal = by_label.get("A.Portal") + assert portal is not None + content_labels = [item["label"] for item in portal.get("content", [])] + assert "A.Portal.IsOpen" in content_labels + + +# --------------------------------------------------------------------------- +# diff_folders — no changes +# --------------------------------------------------------------------------- + + +class TestNoChanges: + def test_empty_changes_when_identical(self): + result = diff_folders(NO_CHANGES, NO_CHANGES) + assert result["changes"] == [] + + +# --------------------------------------------------------------------------- +# diff_folders — missing files are tolerated +# --------------------------------------------------------------------------- + + +class TestMissingFiles: + def test_empty_previous_dir_treated_as_all_added(self, tmp_path: Path): + empty_dir = tmp_path / "empty" + empty_dir.mkdir() + result = diff_folders(empty_dir, PREVIOUS) + # All leaf events must be ADDED; ENTITY MODIFIED may be injected as parent summaries + non_entity_types = {c["change_type"] for c in result["changes"] if c["kind"] not in (ENTITY, "ENUMERATION_SET")} + assert non_entity_types == {ADDED} + + def test_first_run_mode_all_added(self): + result = diff_folders(None, CURRENT) + # All leaf events must be ADDED; ENTITY MODIFIED may be injected as parent summaries + non_entity_types = {c["change_type"] for c in result["changes"] if c["kind"] not in (ENTITY, "ENUMERATION_SET")} + assert non_entity_types == {ADDED} + assert result["previous"] is None + + def test_missing_structs_file_is_skipped(self): + result = diff_folders(PREVIOUS, CURRENT) + # No structs file in either snapshot → no ENUMERATION_SET or struct ENTITY events + struct_changes = [c for c in result["changes"] if c.get("kind") == "ENUMERATION_SET"] + assert struct_changes == [] + + +# --------------------------------------------------------------------------- +# diff_folders — fka type mismatch ignored +# --------------------------------------------------------------------------- + + +class TestFkaTypeMismatch: + def test_fka_with_different_node_type_not_treated_as_rename(self, tmp_path: Path): + prev_dir = tmp_path / "prev" + curr_dir = tmp_path / "curr" + prev_dir.mkdir() + curr_dir.mkdir() + + (prev_dir / "model_snapshot.vspec").write_text( + "A.OldSignal:\n datatype: float\n description: x\n type: sensor\n" + ) + (curr_dir / "model_snapshot.vspec").write_text( + "A.NewBranch:\n description: x\n fka: A.OldSignal\n type: branch\n" + ) + + result = diff_folders(prev_dir, curr_dir) + by_label = changes_by_label(result) + # Type mismatch → no rename; both reported independently + assert by_label["A.OldSignal"]["change_type"] == REMOVED + assert by_label["A.NewBranch"]["change_type"] == ADDED + + +# --------------------------------------------------------------------------- +# diff_folders — datatype array mapping +# --------------------------------------------------------------------------- + + +class TestDatatypeMapping: + def test_array_datatype_sets_is_list(self, tmp_path: Path): + prev_dir = tmp_path / "prev" + curr_dir = tmp_path / "curr" + prev_dir.mkdir() + curr_dir.mkdir() + + (prev_dir / "model_snapshot.vspec").write_text( + "A.Vals:\n datatype: float[]\n description: x\n type: sensor\n" + ) + (curr_dir / "model_snapshot.vspec").write_text( + "A.Vals:\n datatype: float[]\n description: x\n type: sensor\n" + ) + + result = diff_folders(prev_dir, curr_dir) + # No changes between identical snapshots + assert result["changes"] == [] + + def test_added_array_signal_has_is_list_true(self, tmp_path: Path): + prev_dir = tmp_path / "prev" + curr_dir = tmp_path / "curr" + prev_dir.mkdir() + curr_dir.mkdir() + + (prev_dir / "model_snapshot.vspec").write_text("") + (curr_dir / "model_snapshot.vspec").write_text( + "A.Tags:\n datatype: string[]\n description: list of tags\n type: sensor\n" + ) + + result = diff_folders(prev_dir, curr_dir) + by_label = changes_by_label(result) + signal = by_label.get("A.Tags") + assert signal is not None + assert signal["aspects"]["output_type"] == "string" + assert signal["aspects"]["is_list"] is True + + +# --------------------------------------------------------------------------- +# CLI — vspec diff +# --------------------------------------------------------------------------- + + +class TestDiffCli: + def test_cli_outputs_json_to_stdout(self): + runner = CliRunner() + result = runner.invoke(cli, ["-p", str(PREVIOUS), "-c", str(CURRENT)]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + assert "changes" in data + + def test_cli_writes_to_file(self, tmp_path: Path): + out = tmp_path / "diff.json" + runner = CliRunner() + result = runner.invoke(cli, ["-p", str(PREVIOUS), "-c", str(CURRENT), "-o", str(out)]) + assert result.exit_code == 0, result.output + assert out.exists() + data = json.loads(out.read_text()) + assert "changes" in data + + def test_cli_no_changes(self): + runner = CliRunner() + result = runner.invoke(cli, ["-p", str(NO_CHANGES), "-c", str(NO_CHANGES)]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + assert data["changes"] == [] + + def test_cli_first_run_mode_no_previous(self): + runner = CliRunner() + result = runner.invoke(cli, ["-c", str(CURRENT)]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + assert data["previous"] is None + # All leaf events must be ADDED; ENTITY MODIFIED may be injected as parent summaries + non_entity_types = {c["change_type"] for c in data["changes"] if c["kind"] not in (ENTITY, "ENUMERATION_SET")} + assert non_entity_types == {ADDED} + + +# --------------------------------------------------------------------------- +# _expand_instances — Cartesian product +# --------------------------------------------------------------------------- + + +class TestExpandInstances: + """Unit tests for the _expand_instances helper.""" + + def _expand(self, value: Any) -> list[str]: + from vss_tools.diff import _expand_instances + + return _expand_instances(value) + + def test_single_plain_string(self): + assert self._expand("Row[1,3]") == ["Row1", "Row2", "Row3"] + + def test_plain_list_is_single_dimension(self): + assert self._expand(["Left", "Right"]) == ["Left", "Right"] + + def test_range_string_in_list_is_single_dimension(self): + assert self._expand(["Row[1,2]"]) == ["Row1", "Row2"] + + def test_multidimensional_cartesian_product(self): + # ["Row[1,4]", ["DriverSide","PassengerSide"]] → 8 combinations + value = ["Row[1,4]", ["DriverSide", "PassengerSide"]] + result = self._expand(value) + expected = [ + "Row1.DriverSide", + "Row1.PassengerSide", + "Row2.DriverSide", + "Row2.PassengerSide", + "Row3.DriverSide", + "Row3.PassengerSide", + "Row4.DriverSide", + "Row4.PassengerSide", + ] + assert result == expected + + def test_two_range_dimensions(self): + value = ["Row[1,2]", "Pos[1,2]"] + result = self._expand(value) + assert result == ["Row1.Pos1", "Row1.Pos2", "Row2.Pos1", "Row2.Pos2"] + + def test_none_returns_empty(self): + assert self._expand(None) == [] + + def test_multidimensional_in_diff_output(self, tmp_path: Path): + """Cartesian product instances appear correctly in diff_folders output.""" + from vss_tools.diff import diff_folders + + prev_dir = tmp_path / "prev" + curr_dir = tmp_path / "curr" + prev_dir.mkdir() + curr_dir.mkdir() + + (prev_dir / "model_snapshot.vspec").write_text( + "Vehicle.Seat:\n" + " type: branch\n" + " description: seat\n" + " instances:\n" + " - Row[1,2]\n" + " - - DriverSide\n" + " - PassengerSide\n" + ) + (curr_dir / "model_snapshot.vspec").write_text( + "Vehicle.Seat:\n" + " type: branch\n" + " description: seat\n" + " instances:\n" + " - Row[1,2]\n" + " - - DriverSide\n" + " - PassengerSide\n" + ) + + result = diff_folders(prev_dir, curr_dir) + assert result["changes"] == [] + + def test_instances_added_with_cartesian_product(self, tmp_path: Path): + """An ADDED ENTITY node with multi-dimensional instances has Cartesian product in aspects.""" + from vss_tools.diff import ADDED, ENTITY, diff_folders + + prev_dir = tmp_path / "prev" + curr_dir = tmp_path / "curr" + prev_dir.mkdir() + curr_dir.mkdir() + + (prev_dir / "model_snapshot.vspec").write_text("") + (curr_dir / "model_snapshot.vspec").write_text( + "Vehicle.Seat:\n" + " type: branch\n" + " description: seat\n" + " instances:\n" + " - Row[1,2]\n" + " - - DriverSide\n" + " - PassengerSide\n" + ) + + result = diff_folders(prev_dir, curr_dir) + by_label = {c["label"]: c for c in result["changes"]} + seat = by_label.get("Vehicle.Seat") + assert seat is not None + assert seat["change_type"] == ADDED + assert seat["kind"] == ENTITY + assert seat["aspects"]["instances"] == [ + "Row1.DriverSide", + "Row1.PassengerSide", + "Row2.DriverSide", + "Row2.PassengerSide", + ] diff --git a/uv.lock b/uv.lock index 3a862446..d2b625e3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version < '3.12'", ] [[package]] @@ -21,7 +20,6 @@ name = "anyio" version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -122,12 +120,6 @@ dependencies = [ ] 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/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, @@ -169,22 +161,6 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, @@ -291,18 +267,6 @@ version = "7.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/08/bdd7ccca14096f7eb01412b87ac11e5d16e4cb54b6e328afc9dee8bdaec1/coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070", size = 217979, upload-time = "2025-12-08T13:12:14.505Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/d1302e3416298a28b5663ae1117546a745d9d19fde7e28402b2c5c3e2109/coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98", size = 218496, upload-time = "2025-12-08T13:12:16.237Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/d36c354c8b2a320819afcea6bffe72839efd004b98d1d166b90801d49d57/coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5", size = 245237, upload-time = "2025-12-08T13:12:17.858Z" }, - { url = "https://files.pythonhosted.org/packages/91/52/be5e85631e0eec547873d8b08dd67a5f6b111ecfe89a86e40b89b0c1c61c/coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e", size = 247061, upload-time = "2025-12-08T13:12:19.132Z" }, - { url = "https://files.pythonhosted.org/packages/0f/45/a5e8fa0caf05fbd8fa0402470377bff09cc1f026d21c05c71e01295e55ab/coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33", size = 248928, upload-time = "2025-12-08T13:12:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/f5/42/ffb5069b6fd1b95fae482e02f3fecf380d437dd5a39bae09f16d2e2e7e01/coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791", size = 245931, upload-time = "2025-12-08T13:12:22.243Z" }, - { url = "https://files.pythonhosted.org/packages/95/6e/73e809b882c2858f13e55c0c36e94e09ce07e6165d5644588f9517efe333/coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032", size = 246968, upload-time = "2025-12-08T13:12:23.52Z" }, - { url = "https://files.pythonhosted.org/packages/87/08/64ebd9e64b6adb8b4a4662133d706fbaccecab972e0b3ccc23f64e2678ad/coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9", size = 244972, upload-time = "2025-12-08T13:12:24.781Z" }, - { url = "https://files.pythonhosted.org/packages/12/97/f4d27c6fe0cb375a5eced4aabcaef22de74766fb80a3d5d2015139e54b22/coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f", size = 245241, upload-time = "2025-12-08T13:12:28.041Z" }, - { url = "https://files.pythonhosted.org/packages/0c/94/42f8ae7f633bf4c118bf1038d80472f9dade88961a466f290b81250f7ab7/coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8", size = 245847, upload-time = "2025-12-08T13:12:29.337Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2f/6369ca22b6b6d933f4f4d27765d313d8914cc4cce84f82a16436b1a233db/coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f", size = 220573, upload-time = "2025-12-08T13:12:30.905Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dc/a6a741e519acceaeccc70a7f4cfe5d030efc4b222595f0677e101af6f1f3/coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303", size = 221509, upload-time = "2025-12-08T13:12:32.09Z" }, { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, @@ -395,7 +359,6 @@ version = "48.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } wheels = [ @@ -432,13 +395,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" }, ] +[[package]] +name = "ddmodl" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pandas" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "rich-click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/c4/31fbc2f5f589444ff5b2de8ab417fcf7df040ea720e1c97f723603b42275/ddmodl-0.1.0.tar.gz", hash = "sha256:2be308caed0527e3c18fa3c98d1675969af56f29f1573474623577b83ac36531", size = 1183649, upload-time = "2026-07-31T10:08:11.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/25/e9eae9da04c20ab8f81acefadc470d1fc3076fed2e1bef05a5125ce60dc3/ddmodl-0.1.0-py3-none-any.whl", hash = "sha256:fed24cf4ada36941c003a9a76724cf62beaf23670bed93885c8b7c0870952d61", size = 60741, upload-time = "2026-07-31T10:08:12.885Z" }, +] + [[package]] name = "dependency-groups" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/62/55/f054de99871e7beb81935dea8a10b90cd5ce42122b1c3081d5282fdb3621/dependency_groups-1.3.1.tar.gz", hash = "sha256:78078301090517fd938c19f64a53ce98c32834dfe0dee6b88004a569a6adfefd", size = 10093, upload-time = "2025-05-02T00:34:29.452Z" } wheels = [ @@ -463,18 +442,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "filelock" version = "3.20.3" @@ -630,15 +597,6 @@ wheels = [ { 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]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - [[package]] name = "jaraco-classes" version = "3.4.0" @@ -735,16 +693,6 @@ version = "0.7.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/93/e4/b59bdf1197fdf9888452ea4d2048cdad61aef85eb83e99dc52551d7fdc04/librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba", size = 145862, upload-time = "2025-12-15T16:52:43.862Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/1e/3e61dff6c07a3b400fe907d3164b92b3b3023ef86eac1ee236869dc276f7/librt-0.7.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc300cb5a5a01947b1ee8099233156fdccd5001739e5f596ecfbc0dab07b5a3b", size = 54708, upload-time = "2025-12-15T16:51:03.752Z" }, - { url = "https://files.pythonhosted.org/packages/87/98/ab2428b0a80d0fd67decaeea84a5ec920e3dd4d95ecfd074c71f51bd7315/librt-0.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee8d3323d921e0f6919918a97f9b5445a7dfe647270b2629ec1008aa676c0bc0", size = 56656, upload-time = "2025-12-15T16:51:05.038Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ce/de1fad3a16e4fb5b6605bd6cbe6d0e5207cc8eca58993835749a1da0812b/librt-0.7.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:95cb80854a355b284c55f79674f6187cc9574df4dc362524e0cce98c89ee8331", size = 161024, upload-time = "2025-12-15T16:51:06.31Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/ddfcdc1147dd7fb68321d7b064b12f0b9101d85f466a46006f86096fde8d/librt-0.7.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca1caedf8331d8ad6027f93b52d68ed8f8009f5c420c246a46fe9d3be06be0f", size = 169529, upload-time = "2025-12-15T16:51:07.907Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b3/915702c7077df2483b015030d1979404474f490fe9a071e9576f7b26fef6/librt-0.7.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a6f1236151e6fe1da289351b5b5bce49651c91554ecc7b70a947bced6fe212", size = 183270, upload-time = "2025-12-15T16:51:09.164Z" }, - { url = "https://files.pythonhosted.org/packages/45/19/ab2f217e8ec509fca4ea9e2e5022b9f72c1a7b7195f5a5770d299df807ea/librt-0.7.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7766b57aeebaf3f1dac14fdd4a75c9a61f2ed56d8ebeefe4189db1cb9d2a3783", size = 179038, upload-time = "2025-12-15T16:51:10.538Z" }, - { url = "https://files.pythonhosted.org/packages/10/1c/d40851d187662cf50312ebbc0b277c7478dd78dbaaf5ee94056f1d7f2f83/librt-0.7.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1c4c89fb01157dd0a3bfe9e75cd6253b0a1678922befcd664eca0772a4c6c979", size = 173502, upload-time = "2025-12-15T16:51:11.888Z" }, - { url = "https://files.pythonhosted.org/packages/07/52/d5880835c772b22c38db18660420fa6901fd9e9a433b65f0ba9b0f4da764/librt-0.7.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7fa8beef580091c02b4fd26542de046b2abfe0aaefa02e8bcf68acb7618f2b3", size = 193570, upload-time = "2025-12-15T16:51:13.168Z" }, - { url = "https://files.pythonhosted.org/packages/f1/35/22d3c424b82f86ce019c0addadf001d459dfac8036aecc07fadc5c541053/librt-0.7.4-cp310-cp310-win32.whl", hash = "sha256:543c42fa242faae0466fe72d297976f3c710a357a219b1efde3a0539a68a6997", size = 42596, upload-time = "2025-12-15T16:51:14.422Z" }, - { url = "https://files.pythonhosted.org/packages/95/b1/e7c316ac5fe60ac1fdfe515198087205220803c4cf923ee63e1cb8380b17/librt-0.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:25cc40d8eb63f0a7ea4c8f49f524989b9df901969cb860a2bc0e4bad4b8cb8a8", size = 48972, upload-time = "2025-12-15T16:51:15.516Z" }, { url = "https://files.pythonhosted.org/packages/84/64/44089b12d8b4714a7f0e2f33fb19285ba87702d4be0829f20b36ebeeee07/librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a", size = 54709, upload-time = "2025-12-15T16:51:16.699Z" }, { url = "https://files.pythonhosted.org/packages/26/ef/6fa39fb5f37002f7d25e0da4f24d41b457582beea9369eeb7e9e73db5508/librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729", size = 56663, upload-time = "2025-12-15T16:51:17.856Z" }, { url = "https://files.pythonhosted.org/packages/9d/e4/cbaca170a13bee2469c90df9e47108610b4422c453aea1aec1779ac36c24/librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed", size = 161703, upload-time = "2025-12-15T16:51:19.421Z" }, @@ -840,17 +788,10 @@ dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, @@ -940,7 +881,6 @@ dependencies = [ { name = "dependency-groups" }, { name = "humanize" }, { name = "packaging" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/a8/e169497599266d176832e2232c08557ffba97eef87bf8a18f9f918e0c6aa/nox-2025.11.12.tar.gz", hash = "sha256:3d317f9e61f49d6bde39cf2f59695bb4e1722960457eee3ae19dacfe03c07259", size = 4030561, upload-time = "2025-11-12T18:39:03.319Z" } @@ -948,79 +888,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/34/434c594e0125a16b05a7bedaea33e63c90abbfbe47e5729a735a8a8a90ea/nox-2025.11.12-py3-none-any.whl", hash = "sha256:707171f9f63bc685da9d00edd8c2ceec8405b8e38b5fb4e46114a860070ef0ff", size = 74447, upload-time = "2025-11-12T18:39:01.575Z" }, ] -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - [[package]] name = "numpy" version = "2.3.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, @@ -1112,21 +983,13 @@ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, - { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, - { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, - { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, - { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, @@ -1257,19 +1120,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, @@ -1348,14 +1198,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, @@ -1404,12 +1246,10 @@ version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ @@ -1475,15 +1315,6 @@ 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, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, @@ -1550,7 +1381,6 @@ name = "rdflib" version = "7.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "isodate", marker = "python_full_version < '3.11'" }, { name = "pyparsing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ec/1b/4cd9a29841951371304828d13282e27a5f25993702c7c87dcb7e0604bd25/rdflib-7.5.0.tar.gz", hash = "sha256:663083443908b1830e567350d72e74d9948b310f827966358d76eebdc92bf592", size = 4903859, upload-time = "2025-11-28T05:51:54.562Z" } @@ -1643,7 +1473,6 @@ dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/d8/f2c1b7e9a645ba40f756d7a5b195fc104729bc6b19061ba3ab385f342931/rich_click-1.9.4.tar.gz", hash = "sha256:af73dc68e85f3bebb80ce302a642b9fe3b65f3df0ceb42eb9a27c467c1b678c8", size = 73632, upload-time = "2025-10-25T01:08:49.142Z" } wheels = [ @@ -1656,20 +1485,6 @@ version = "0.30.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, @@ -1975,7 +1790,6 @@ dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ @@ -1990,6 +1804,7 @@ dependencies = [ { name = "anytree" }, { name = "case-converter" }, { name = "click" }, + { name = "ddmodl" }, { name = "graphene" }, { name = "graphql-core" }, { name = "inflect" }, @@ -2021,6 +1836,7 @@ requires-dist = [ { name = "anytree", specifier = ">=2.12.1" }, { name = "case-converter", specifier = ">=1.2.0" }, { name = "click", specifier = ">=8.1.7" }, + { name = "ddmodl" }, { name = "graphene", specifier = ">=3.4.3" }, { name = "graphql-core", specifier = ">=3.2.5" }, { name = "inflect", specifier = ">=7.5.0" },