diff --git a/.github/workflows/commit_checks.yaml b/.github/workflows/commit_checks.yaml index 86057c0..e2c998b 100644 --- a/.github/workflows/commit_checks.yaml +++ b/.github/workflows/commit_checks.yaml @@ -25,7 +25,7 @@ jobs: typecheck: needs: commitlint - name: PyreFly Type Checker + name: ty Type Checker runs-on: ubuntu-24.04 steps: @@ -35,15 +35,13 @@ jobs: - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: enable-cache: true + activate-environment: "true" + python-version: "3.13" - - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: '3.13' - - - name: Run Pyrefly Type Checker + - name: Run ty Type Checker run: | uv sync - uv run pyrefly check + uv run ty check --output-format github ruff: needs: commitlint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9ca058..0f8e88c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - '--fix=lf' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.14.10' + rev: 'v0.15.1' hooks: - id: ruff args: [--fix] @@ -26,7 +26,7 @@ repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.9.18 + rev: 0.10.3 hooks: - id: uv-lock @@ -34,11 +34,3 @@ repos: rev: v2.4.1 hooks: - id: codespell - - - repo: https://github.com/facebook/pyrefly-pre-commit - rev: 0.46.0 - hooks: - - id: pyrefly-check - name: Pyrefly (type checking) - pass_filenames: false - language: system diff --git a/examples/ort_result.py b/examples/ort_result.py index cfe7746..cf80bdd 100644 --- a/examples/ort_result.py +++ b/examples/ort_result.py @@ -17,12 +17,23 @@ @click.command() @click.argument("datafile") -def main(datafile: str) -> None: +@click.option("-a", "--analyzer", is_flag=True) +@click.option("-v", "--advisor", is_flag=True) +def main( + datafile: str, + analyzer: bool, + advisor: bool, +) -> None: try: with Path(datafile).open() as fd: data = yaml.safe_load(fd) parsed = OrtResult(**data) - pprint(parsed) + if analyzer: + pprint(parsed.analyzer) + if advisor: + pprint(parsed.advisor) + else: + pprint(parsed) except ValidationError as e: logger.error(e) sys.exit(1) diff --git a/examples/repo_config.py b/examples/repo_config.py index 41bfa2b..a6c8083 100644 --- a/examples/repo_config.py +++ b/examples/repo_config.py @@ -10,7 +10,7 @@ from pydantic import ValidationError from rich.pretty import pprint -from ort import OrtRepositoryConfiguration +from ort import RepositoryConfiguration logger = logging.getLogger() @@ -21,7 +21,7 @@ def main(datafile: str) -> None: try: with Path(datafile).open() as fd: data = yaml.safe_load(fd) - parsed = OrtRepositoryConfiguration(**data) + parsed = RepositoryConfiguration(**data) pprint(parsed) except ValidationError as e: logger.error(e) diff --git a/pyproject.toml b/pyproject.toml index 46df6b2..1db8c1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "uv_build" [project] name = "python-ort" -version = "0.5.0" +version = "0.6.0" description = "A Python Ort model serialization library" readme = "README.md" license = "MIT" @@ -31,13 +31,11 @@ module-root = "src" [dependency-groups] dev = [ - "datamodel-code-generator[http]>=0.53.0", - "pre-commit>=4.5.1", - "pycodestyle>=2.14.0", - "pyrefly>=0.49.0", + "datamodel-code-generator[http]>=0.54.0", "pytest>=9.0.2", - "rich>=14.2.0", - "ruff>=0.14.14", + "rich>=14.3.2", + "ruff>=0.15.1", + "ty>=0.0.17", "types-pyyaml>=6.0.12.20250915", ] diff --git a/python-ort.code-workspace b/python-ort.code-workspace deleted file mode 100644 index 5709732..0000000 --- a/python-ort.code-workspace +++ /dev/null @@ -1,8 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": {} -} diff --git a/src/ort/__init__.py b/src/ort/__init__.py index 2e3fcd3..e5c2f0b 100644 --- a/src/ort/__init__.py +++ b/src/ort/__init__.py @@ -4,10 +4,10 @@ from ort.models.analyzer_result import AnalyzerResult from ort.models.ort_result import OrtResult -from ort.models.repository_configuration import OrtRepositoryConfiguration +from ort.models.repository_configuration import RepositoryConfiguration __all__ = [ "AnalyzerResult", - "OrtRepositoryConfiguration", + "RepositoryConfiguration", "OrtResult", ] diff --git a/src/ort/models/__init__.py b/src/ort/models/__init__.py index a51a053..f4d4356 100644 --- a/src/ort/models/__init__.py +++ b/src/ort/models/__init__.py @@ -1,10 +1,64 @@ # SPDX-FileCopyrightText: 2025 Helio Chissini de Castro # SPDX-License-Identifier: MIT +from .advisor_capability import AdvisorCapability +from .advisor_result import AdvisorResult +from .advisor_run import AdvisorRun +from .analyzer_result import AnalyzerResult +from .analyzer_run import AnalyzerRun +from .dependency_graph import DependencyGraph +from .dependency_graph_edge import DependencyGraphEdge +from .dependency_graph_node import DependencyGraphNode +from .dependency_reference import DependencyReference +from .hash import Hash +from .hash_algorithm import HashAlgorithm from .identifier import Identifier +from .issue import Issue +from .ort_result import OrtResult +from .package import Package +from .package_curation import PackageCuration +from .package_curation_data import PackageCurationData +from .package_linkage import PackageLinkage +from .package_reference import PackageReference +from .project import Project +from .remote_artifact import RemoteArtifact +from .repository import Repository +from .repository_configuration import RepositoryConfiguration +from .root_dependency_index import RootDependencyIndex +from .scope import Scope +from .source_code_origin import SourceCodeOrigin +from .vcsinfo import VcsInfo +from .vcsinfo_curation_data import VcsInfoCurationData from .vcstype import VcsType __all__ = [ + "AdvisorCapability", + "AdvisorResult", + "AdvisorRun", + "AnalyzerResult", + "AnalyzerRun", + "DependencyGraph", + "DependencyGraphEdge", + "DependencyGraphNode", + "DependencyReference", + "Hash", + "HashAlgorithm", "Identifier", + "Issue", + "OrtResult", + "Package", + "PackageCuration", + "PackageCurationData", + "PackageLinkage", + "PackageReference", + "Project", + "RemoteArtifact", + "Repository", + "RepositoryConfiguration", + "RootDependencyIndex", + "Scope", + "SourceCodeOrigin", + "VcsInfo", + "VcsInfoCurationData", "VcsType", ] diff --git a/src/ort/models/advisor_capability.py b/src/ort/models/advisor_capability.py new file mode 100644 index 0000000..dd9d5f5 --- /dev/null +++ b/src/ort/models/advisor_capability.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + + +from enum import IntEnum + + +class AdvisorCapability(IntEnum): + """ + An enum class that defines the capabilities of a specific advisor implementation. + + There are multiple types of findings that can be retrieved by an advisor, such as security vulnerabilities or + defects. An [AdvisorResult] has different fields for the different findings types. This enum corresponds to these + fields. It allows an advisor implementation to declare, which of these fields it can populate. This information is + of interest, for instance, when generating reports for specific findings to determine, which advisor may have + contributed. + + """ + + DEFECTS = 1 + VULNERABILITIES = 2 diff --git a/src/ort/models/advisor_details.py b/src/ort/models/advisor_details.py new file mode 100644 index 0000000..59e5433 --- /dev/null +++ b/src/ort/models/advisor_details.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from ort.models import AdvisorCapability + + +class AdvisorDetails(BaseModel): + """ + Details about the used provider of vulnerability information. + + """ + + model_config = ConfigDict( + extra="forbid", + ) + + name: str = Field(description="The name of the used advisor.") + capabilities: set[AdvisorCapability] = Field( + description="The capabilities of the used advisor. This property indicates, which kind of findings" + "are retrieved by the advisor." + ) + + @field_validator("capabilities", mode="before") + @classmethod + def convert_capability(cls, v): + def _convert(item): + if isinstance(item, str): + try: + return AdvisorCapability[item] + except KeyError: + raise ValueError(f"Invalid capability: {item}") + return item + + if isinstance(v, (list, set)): + return {_convert(item) for item in v} + if isinstance(v, str): + return _convert(v) + return v diff --git a/src/ort/models/advisor_result.py b/src/ort/models/advisor_result.py new file mode 100644 index 0000000..beeba25 --- /dev/null +++ b/src/ort/models/advisor_result.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from pydantic import BaseModel, ConfigDict, Field + +from ort.models.vulnerabilities import Vulnerability + +from .advisor_details import AdvisorDetails +from .advisor_summary import AdvisorSummary +from .defect import Defect + + +class AdvisorResult(BaseModel): + """ + The result of a specific advisor execution for a single package. + + Different advisor implementations may produce findings of different types. To reflect this, this class has multiple + fields for findings of these types. It is up to a concrete advisor, which of these fields it populates. + + """ + + model_config = ConfigDict( + extra="forbid", + ) + + advisor: AdvisorDetails = Field( + description="Details about the used advisor.", + ) + + summary: AdvisorSummary = Field( + description="A summary of the advisor results.", + ) + + defects: list[Defect] = Field( + default_factory=list, + description="The defects.", + ) + + vulnerabilities: list[Vulnerability] = Field( + default_factory=list, + description="The vulnerabilities.", + ) diff --git a/src/ort/models/advisor_run.py b/src/ort/models/advisor_run.py new file mode 100644 index 0000000..8832c95 --- /dev/null +++ b/src/ort/models/advisor_run.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from datetime import datetime + +from pydantic import BaseModel, ConfigDict, Field + +from ort.models import AdvisorResult +from ort.models.config.advisor_configuration import AdvisorConfiguration +from ort.utils.environment import Environment + +from .identifier import Identifier + + +class AdvisorRun(BaseModel): + """ + Type alias for a function that allows filtering of [AdvisorResult]s. + + """ + + model_config = ConfigDict( + extra="forbid", + ) + start_time: datetime = Field( + description="The time the advisor was started.", + ) + end_time: datetime = Field( + description="The time the advisor has finished.", + ) + environment: Environment = Field( + description="The [Environment] in which the advisor was executed.", + ) + config: AdvisorConfiguration = Field( + description="The [AdvisorConfiguration] used for this run.", + ) + results: dict[Identifier, list[AdvisorResult]] = Field( + default_factory=dict, + description="The result of this run.", + ) diff --git a/src/ort/models/advisor_summary.py b/src/ort/models/advisor_summary.py new file mode 100644 index 0000000..c27d79b --- /dev/null +++ b/src/ort/models/advisor_summary.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from datetime import datetime + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from .issue import Issue + + +class AdvisorSummary(BaseModel): + """ + A short summary of the advisor result. + + """ + + model_config = ConfigDict( + extra="forbid", + ) + + start_time: datetime = Field( + description="The time the advisor started.", + ) + end_time: datetime = Field( + description="The time the advisor finished.", + ) + issues: list[Issue] = Field( + default_factory=list, + description="The list of issues that occurred during the advisor run." + "This property is not serialized if the list is empty to reduce the size of the result file.", + ) + + @field_validator("start_time", "end_time", mode="before") + @classmethod + def transform_date(cls, v): + if isinstance(v, str): + return datetime.fromisoformat(v.replace("Z", "+00:00")) + return v diff --git a/src/ort/models/analyzer_result.py b/src/ort/models/analyzer_result.py index 784e037..d27cef9 100644 --- a/src/ort/models/analyzer_result.py +++ b/src/ort/models/analyzer_result.py @@ -4,11 +4,11 @@ from pydantic import BaseModel, ConfigDict, Field -from ort.models.dependency_graph import DependencyGraph -from ort.models.identifier import Identifier -from ort.models.issue import Issue -from ort.models.package import Package -from ort.models.project import Project +from .dependency_graph import DependencyGraph +from .identifier import Identifier +from .issue import Issue +from .package import Package +from .project import Project class AnalyzerResult(BaseModel): diff --git a/src/ort/models/analyzer_run.py b/src/ort/models/analyzer_run.py index 00f0891..1eeea99 100644 --- a/src/ort/models/analyzer_run.py +++ b/src/ort/models/analyzer_run.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, ConfigDict, Field -from ort.models.analyzer_result import AnalyzerResult +from ort.models import AnalyzerResult from ort.models.config.analyzer_configuration import AnalyzerConfiguration from ort.utils.environment import Environment diff --git a/src/ort/models/config/advisor_configuration.py b/src/ort/models/config/advisor_configuration.py new file mode 100644 index 0000000..72a4c46 --- /dev/null +++ b/src/ort/models/config/advisor_configuration.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + + +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field + + +class AdvisorConfiguration(BaseModel): + """ + The configuration model of the advisor. This class is (de-)serialized in the following places: + - Deserialized from "config.yml" as part of [OrtConfiguration]. + - (De-)Serialized as part of [org.ossreviewtoolkit.model.OrtResult]. + """ + + model_config = ConfigDict( + extra="forbid", + ) + skip_excluded: bool = Field( + default=False, + description="A flag to control whether excluded scopes and paths should be skipped when giving the advice.", + ) + advisors: dict[str, Any] | None = Field( + default=None, + description="A map with [configuration][PluginConfig] for advice providers using the" + "[plugin id][PluginDescriptor.id] as key.", + ) diff --git a/src/ort/models/defect.py b/src/ort/models/defect.py new file mode 100644 index 0000000..572e31d --- /dev/null +++ b/src/ort/models/defect.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from datetime import datetime + +from pydantic import AnyUrl, BaseModel, ConfigDict, Field + + +class Defect(BaseModel): + """ + A data model for software defects. + + Instances of this class are created by advisor implementations that retrieve information about + known defects in packages. + + """ + + model_config = ConfigDict( + extra="forbid", + ) + + id: str = Field( + description="The (external) ID of this defect. This is a string used by a concrete issue tracker" + "system to reference this defect, such as a bug ID or ticket number.", + ) + + url: AnyUrl = Field( + description="The URL pointing to the source of this defect. This is typically a reference into " + "the issue tracker system that contains this defect.", + ) + title: str | None = Field( + default=None, + description="A title for this defect if available. This is a short summary describing the problem at hand.", + ) + state: str | None = Field( + default=None, + description="A state of the associated defect if available. The concrete meaning of this string depends" + "on the source from where it was obtained, as different issue tracker systems use their specific " + "terminology. Possible values could be OPEN, IN PROGRESS, BLOCKED, etc.", + ) + severity: str | None = Field( + default=None, + description="The severity assigned to the defect if available. The meaning of this string depends" + "on the source system.", + ) + description: str | None = Field( + default=None, + description="An optional description of this defect. It can contain more detailed information about" + "the defect and its impact. The field may be undefined if the url of this defect already points to" + "a website with all this information.", + ) + creation_time: datetime | None = Field( + default=None, + description="The creation time of this defect if available.", + ) + modification_time: datetime | None = Field( + default=None, + description="Contains a time when this defect has been modified the last time in the tracker system" + "it has been obtained from. This information can be useful for instance to find out how up-to-date" + "this defect report might be.", + ) + closing_time: datetime | None = Field( + default=None, + description="Contains a time when this defect has been closed if it has been resolved already" + "(and this information is available in the source system). For users of the component affected" + "by this defect, this information can be of interest to find out whether a fix is available," + "maybe in a newer version.", + ) + fix_release_version: str | None = Field( + default=None, + description="Contains the version of the release, in which this defect was fixed if available." + "This is important information for consumers of the component affected by the defect, so they" + "can upgrade to this version.", + ) + fix_release_url: AnyUrl | None = Field( + default=None, + description="A URL pointing to the release, in which this defect was fixed if available." + "Depending on the information provided by a source, this URL could point to a website with detail" + "information about the release, to release notes, or something like that. This information is" + "important for consumers of the component affected by this defect, so they can upgrade to this release.", + ) + labels: dict[str, str] = Field( + default_factory=dict, + description="A map with labels assigned to this defect. Labels provide a means frequently used by issue" + "tracker systems to classify defects based on defined criteria. The exact meaning of these labels is" + "depending on the source system.", + ) diff --git a/src/ort/models/dependency_graph.py b/src/ort/models/dependency_graph.py index 4a4be5b..2524aa0 100644 --- a/src/ort/models/dependency_graph.py +++ b/src/ort/models/dependency_graph.py @@ -4,11 +4,11 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator -from ort.models.dependency_graph_edge import DependencyGraphEdge -from ort.models.dependency_graph_node import DependencyGraphNode -from ort.models.dependency_reference import DependencyReference -from ort.models.identifier import Identifier -from ort.models.root_dependency_index import RootDependencyIndex +from .dependency_graph_edge import DependencyGraphEdge +from .dependency_graph_node import DependencyGraphNode +from .dependency_reference import DependencyReference +from .identifier import Identifier +from .root_dependency_index import RootDependencyIndex class DependencyGraph(BaseModel): diff --git a/src/ort/models/dependency_graph_node.py b/src/ort/models/dependency_graph_node.py index 7ba9099..e25bcc7 100644 --- a/src/ort/models/dependency_graph_node.py +++ b/src/ort/models/dependency_graph_node.py @@ -2,10 +2,10 @@ # SPDX-License-Identifier: MIT -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator -from ort.models.issue import Issue -from ort.models.package_linkage import PackageLinkage +from .issue import Issue +from .package_linkage import PackageLinkage class DependencyGraphNode(BaseModel): @@ -42,3 +42,13 @@ class DependencyGraphNode(BaseModel): issues: list[Issue] = Field( default_factory=list, description="A list of Issue objects that occurred handling this dependency." ) + + @field_validator("linkage", mode="before") + @classmethod + def convert_linkage(cls, v): + if isinstance(v, str): + try: + return PackageLinkage[v] + except KeyError: + raise ValueError(f"Invalid linkage: {v}") + return v diff --git a/src/ort/models/dependency_reference.py b/src/ort/models/dependency_reference.py index 58c793b..a0ce23e 100644 --- a/src/ort/models/dependency_reference.py +++ b/src/ort/models/dependency_reference.py @@ -4,8 +4,8 @@ from pydantic import BaseModel, ConfigDict, Field -from ort.models.issue import Issue -from ort.models.package_linkage import PackageLinkage +from .issue import Issue +from .package_linkage import PackageLinkage class DependencyReference(BaseModel): diff --git a/src/ort/models/hash.py b/src/ort/models/hash.py index 23dc0ee..413b8dc 100644 --- a/src/ort/models/hash.py +++ b/src/ort/models/hash.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field -from ort.models.hash_algorithm import HashAlgorithm +from .hash_algorithm import HashAlgorithm class Hash(BaseModel): diff --git a/src/ort/models/ort_result.py b/src/ort/models/ort_result.py index 2a6de4c..0632c5f 100644 --- a/src/ort/models/ort_result.py +++ b/src/ort/models/ort_result.py @@ -4,8 +4,9 @@ from pydantic import BaseModel, ConfigDict, Field -from ort.models.analyzer_run import AnalyzerRun -from ort.models.repository import Repository +from .advisor_run import AdvisorRun +from .analyzer_run import AnalyzerRun +from .repository import Repository class OrtResult(BaseModel): @@ -30,3 +31,14 @@ class OrtResult(BaseModel): description="An [AnalyzerRun] containing details about the analyzer that was run using [repository]" "as input. Can be null if the [repository] was not yet analyzed." ) + advisor: AdvisorRun | None = Field( + default=None, + description="An [AdvisorRun] containing details about the advisor that was run using the result from" + "[analyzer] as input. Can be null if no advisor was run.", + ) + labels: dict[str, str] = Field( + default_factory=dict, + description="User defined labels associated to this result. Labels are not used by ORT itself, " + "but can be used in parts of ORT which are customizable by the user, for example in evaluator" + "rules or in the notice reporter.", + ) diff --git a/src/ort/models/package.py b/src/ort/models/package.py index ce8c96f..650770e 100644 --- a/src/ort/models/package.py +++ b/src/ort/models/package.py @@ -4,12 +4,13 @@ from pydantic import BaseModel, ConfigDict, Field -from ort.models.identifier import Identifier -from ort.models.remote_artifact import RemoteArtifact -from ort.models.source_code_origin import SourceCodeOrigin -from ort.models.vcsinfo import VcsInfo from ort.utils.processed_declared_license import ProcessedDeclaredLicense +from .identifier import Identifier +from .remote_artifact import RemoteArtifact +from .source_code_origin import SourceCodeOrigin +from .vcsinfo import VcsInfo + class Package(BaseModel): """ diff --git a/src/ort/models/package_curation_data.py b/src/ort/models/package_curation_data.py index 1ae1a1d..2494afa 100644 --- a/src/ort/models/package_curation_data.py +++ b/src/ort/models/package_curation_data.py @@ -3,18 +3,13 @@ from typing import Any -from pydantic import AnyUrl, BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field -from .hash import Hash +from .remote_artifact import RemoteArtifact from .source_code_origin import SourceCodeOrigin from .vcsinfo_curation_data import VcsInfoCurationData -class CurationArtifact(BaseModel): - url: AnyUrl - hash: Hash - - class PackageCurationData(BaseModel): """ Data model for package curation data. @@ -47,8 +42,8 @@ class PackageCurationData(BaseModel): concluded_license: str | None = None description: str | None = None homepage_url: str | None = None - binary_artifact: CurationArtifact | None = None - source_artifact: CurationArtifact | None = None + binary_artifact: RemoteArtifact | None = None + source_artifact: RemoteArtifact | None = None vcs: VcsInfoCurationData | None = None is_metadata_only: bool | None = None is_modified: bool | None = None diff --git a/src/ort/models/package_linkage.py b/src/ort/models/package_linkage.py index f4625e2..525beec 100644 --- a/src/ort/models/package_linkage.py +++ b/src/ort/models/package_linkage.py @@ -2,10 +2,10 @@ # SPDX-License-Identifier: MIT -from enum import Enum, auto +from enum import IntEnum -class PackageLinkage(Enum): +class PackageLinkage(IntEnum): """ A class to denote the linkage type between two packages. @@ -27,7 +27,7 @@ class PackageLinkage(Enum): e.g. a subproject of a multi-project. """ - DYNAMIC = auto() - STATIC = auto() - PROJECT_DYNAMIC = auto() - PROJECT_STATIC = auto() + DYNAMIC = 1 + STATIC = 2 + PROJECT_DYNAMIC = 3 + PROJECT_STATIC = 4 diff --git a/src/ort/models/package_reference.py b/src/ort/models/package_reference.py index 876783f..019e5b9 100644 --- a/src/ort/models/package_reference.py +++ b/src/ort/models/package_reference.py @@ -4,8 +4,8 @@ from pydantic import BaseModel, ConfigDict, Field -from ort.models.issue import Issue -from ort.models.package_linkage import PackageLinkage +from .issue import Issue +from .package_linkage import PackageLinkage class PackageReference(BaseModel): diff --git a/src/ort/models/project.py b/src/ort/models/project.py index 5d39544..d0e2ff6 100644 --- a/src/ort/models/project.py +++ b/src/ort/models/project.py @@ -4,11 +4,12 @@ from pydantic import BaseModel, ConfigDict, Field -from ort.models.identifier import Identifier -from ort.models.scope import Scope -from ort.models.vcsinfo import VcsInfo from ort.utils.processed_declared_license import ProcessedDeclaredLicense +from .identifier import Identifier +from .scope import Scope +from .vcsinfo import VcsInfo + class Project(BaseModel): """ diff --git a/src/ort/models/remote_artifact.py b/src/ort/models/remote_artifact.py index aa130d6..5b0a8e9 100644 --- a/src/ort/models/remote_artifact.py +++ b/src/ort/models/remote_artifact.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field -from ort.models.hash import Hash +from .hash import Hash class RemoteArtifact(BaseModel): @@ -13,8 +13,10 @@ class RemoteArtifact(BaseModel): """ url: str = Field( + default_factory=str, description="The URL of the remote artifact.", ) - hash: Hash = Field( + hash: Hash | None = Field( + default=None, description="The hash of the remote artifact.", ) diff --git a/src/ort/models/repository.py b/src/ort/models/repository.py index 53abea4..3c97a3f 100644 --- a/src/ort/models/repository.py +++ b/src/ort/models/repository.py @@ -4,8 +4,8 @@ from pydantic import BaseModel, ConfigDict, Field -from ort.models.repository_configuration import OrtRepositoryConfiguration -from ort.models.vcsinfo import VcsInfo +from .repository_configuration import RepositoryConfiguration +from .vcsinfo import VcsInfo class Repository(BaseModel): @@ -37,6 +37,6 @@ class Repository(BaseModel): description="A map of nested repositories, for example Git submodules or Git-Repo" "modules. The key is the path to the nested repository relative to the root of the main repository.", ) - config: OrtRepositoryConfiguration = Field( + config: RepositoryConfiguration = Field( description="The configuration of the repository, parsed from [ORT_REPO_CONFIG_FILENAME]." ) diff --git a/src/ort/models/repository_configuration.py b/src/ort/models/repository_configuration.py index 2c517bb..5031bd9 100644 --- a/src/ort/models/repository_configuration.py +++ b/src/ort/models/repository_configuration.py @@ -260,7 +260,7 @@ class OrtRepositoryConfigurationCurations1(BaseModel): packages: Curations -class OrtRepositoryConfiguration(BaseModel): +class RepositoryConfiguration(BaseModel): """ Represents the configuration for an OSS-Review-Toolkit (ORT) repository. diff --git a/src/ort/models/scope.py b/src/ort/models/scope.py index fec901a..a3545a6 100644 --- a/src/ort/models/scope.py +++ b/src/ort/models/scope.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, ConfigDict, Field -from ort.models.package_reference import PackageReference +from .package_reference import PackageReference class Scope(BaseModel): diff --git a/src/ort/models/vcsinfo.py b/src/ort/models/vcsinfo.py index 234d105..e981780 100644 --- a/src/ort/models/vcsinfo.py +++ b/src/ort/models/vcsinfo.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025 Helio Chissini de Castro # SPDX-License-Identifier: MIT -from pydantic import AnyUrl, BaseModel, Field +from pydantic import BaseModel, Field from .vcstype import VcsType @@ -22,7 +22,8 @@ class VcsInfo(BaseModel): default_factory=VcsType, description="The type of the VCS, for example Git, GitRepo, Mercurial, etc.", ) - url: AnyUrl = Field( + url: str = Field( + default_factory=str, description="The URL to the VCS repository.", ) diff --git a/src/ort/models/vulnerabilities/__init__.py b/src/ort/models/vulnerabilities/__init__.py new file mode 100644 index 0000000..f2a00d1 --- /dev/null +++ b/src/ort/models/vulnerabilities/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from .vulnerability import Vulnerability + +__all__ = [ + "Vulnerability", +] diff --git a/src/ort/models/vulnerabilities/cvss2_rating.py b/src/ort/models/vulnerabilities/cvss2_rating.py new file mode 100644 index 0000000..e11edde --- /dev/null +++ b/src/ort/models/vulnerabilities/cvss2_rating.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from enum import Enum + + +class Cvss2Rating(Enum): + """ + A CVSS version 2 rating, see https://nvd.nist.gov/vuln-metrics/cvss. + """ + + LOW = 4.0 + MEDIUM = 7.0 + HIGH = 10.0 + + def __init__(self, upper_bound: float): + self._upper_bound = upper_bound + + @classmethod + def prefixes(cls) -> set[str]: + """A set of prefixes that refer to the CVSS version 2 scoring system.""" + return {"CVSS2", "CVSSV2", "CVSS_V2", "CVSS:2"} + + @classmethod + def from_score(cls, score: float) -> "Cvss2Rating | None": + """Get the Cvss2Rating from a score, or None if the score does not map to any Cvss2Rating.""" + if score < 0.0 or score > cls.HIGH.upper_bound: + return None + if score < cls.LOW.upper_bound: + return cls.LOW + if score < cls.MEDIUM.upper_bound: + return cls.MEDIUM + if score <= cls.HIGH.upper_bound: + return cls.HIGH + return None + + @property + def upper_bound(self) -> float: + return self._upper_bound diff --git a/src/ort/models/vulnerabilities/cvss3_rating.py b/src/ort/models/vulnerabilities/cvss3_rating.py new file mode 100644 index 0000000..49dcb50 --- /dev/null +++ b/src/ort/models/vulnerabilities/cvss3_rating.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from enum import Enum + + +class Cvss3Rating(Enum): + """ + A CVSS version 3 rating as defined by https://www.first.org/cvss/v3.1/specification-document#Qualitative-Severity-Rating-Scale. + """ + + NONE = 0.0 + LOW = 4.0 + MEDIUM = 7.0 + HIGH = 9.0 + CRITICAL = 10.0 + + def __init__(self, upper_bound: float): + self._upper_bound = upper_bound + + @classmethod + def prefixes(cls) -> set[str]: + """A set of prefixes that refer to the CVSS version 3 scoring system.""" + return {"CVSS3", "CVSSV3", "CVSS_V3", "CVSS:3"} + + @classmethod + def from_score(cls, score: float) -> "Cvss3Rating | None": + """Get the Cvss3Rating from a score, or None if the score does not map to any Cvss3Rating.""" + if score < 0.0 or score > cls.CRITICAL.upper_bound: + return None + if score < cls.NONE.upper_bound: + return cls.NONE + if score < cls.LOW.upper_bound: + return cls.LOW + if score < cls.MEDIUM.upper_bound: + return cls.MEDIUM + if score < cls.HIGH.upper_bound: + return cls.HIGH + if score <= cls.CRITICAL.upper_bound: + return cls.CRITICAL + return None + + @property + def upper_bound(self) -> float: + return self._upper_bound diff --git a/src/ort/models/vulnerabilities/cvss4_rating.py b/src/ort/models/vulnerabilities/cvss4_rating.py new file mode 100644 index 0000000..0b08eda --- /dev/null +++ b/src/ort/models/vulnerabilities/cvss4_rating.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from enum import Enum + + +class Cvss4Rating(Enum): + """ + A CVSS version 4 rating as defined by https://www.first.org/cvss/v4.0/specification-document#Qualitative-Severity-Rating-Scale. + """ + + NONE = 0.0 + LOW = 4.0 + MEDIUM = 7.0 + HIGH = 9.0 + CRITICAL = 10.0 + + def __init__(self, upper_bound: float): + self._upper_bound = upper_bound + + @classmethod + def prefixes(cls) -> set[str]: + """A set of prefixes that refer to the CVSS version 4 scoring system.""" + return {"CVSS4", "CVSSV4", "CVSS_V4", "CVSS:4"} + + @classmethod + def from_score(cls, score: float) -> "Cvss4Rating | None": + """Get the Cvss4Rating from a score, or None if the score does not map to any Cvss4Rating.""" + if score < 0.0 or score > cls.CRITICAL.upper_bound: + return None + if score < cls.NONE.upper_bound: + return cls.NONE + if score < cls.LOW.upper_bound: + return cls.LOW + if score < cls.MEDIUM.upper_bound: + return cls.MEDIUM + if score < cls.HIGH.upper_bound: + return cls.HIGH + if score <= cls.CRITICAL.upper_bound: + return cls.CRITICAL + return None + + @property + def upper_bound(self) -> float: + return self._upper_bound diff --git a/src/ort/models/vulnerabilities/vulnerability.py b/src/ort/models/vulnerabilities/vulnerability.py new file mode 100644 index 0000000..0f7d3c8 --- /dev/null +++ b/src/ort/models/vulnerabilities/vulnerability.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from pydantic import BaseModel, ConfigDict, Field + +from .vulnerability_reference import VulnerabilityReference + + +class Vulnerability(BaseModel): + """ + Base model for all vulnerability providers supported by the advisor. + + This class stores the information about a single vulnerability, which may have been retrieved from multiple + vulnerability providers. For each source of information a [VulnerabilityReference] is contained. + """ + + model_config = ConfigDict( + extra="forbid", + ) + + id: str = Field( + ..., + description="The ID of a vulnerability. Most likely a CVE identifier.", + ) + + summary: str | None = Field( + None, + description="A short summary of this Vulnerability.", + ) + + description: str | None = Field( + None, + description="A full description of this Vulnerability.", + ) + + references: list["VulnerabilityReference"] = Field( + ..., + description="A list with detailed information for this vulnerability obtained from different sources.", + ) diff --git a/src/ort/models/vulnerabilities/vulnerability_reference.py b/src/ort/models/vulnerabilities/vulnerability_reference.py new file mode 100644 index 0000000..0d7868c --- /dev/null +++ b/src/ort/models/vulnerabilities/vulnerability_reference.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel, ConfigDict, Field + +from .cvss2_rating import Cvss2Rating +from .cvss3_rating import Cvss3Rating +from .cvss4_rating import Cvss4Rating + + +class VulnerabilityReference(BaseModel): + """ + A data class representing detailed information about a vulnerability obtained from a specific source. + + A single vulnerability can be listed by multiple sources using different scoring systems to denote its severity. + So when ORT queries different providers for vulnerability information it may well find multiple records for a single + vulnerability, which could even contain contradicting information. To model this, a [Vulnerability] is associated + with a list of references; each reference points to the source of the information and has some detailed information + provided by this source. + + """ + + model_config = ConfigDict( + extra="forbid", + ) + + url: str = Field( + ..., + description="The URI pointing to details of the belonging vulnerability.", + ) + + scoring_system: str | None = Field( + default=None, + description="The name of the scoring system (like CVSS, EPSS), if any, as reported by the advice provider.", + ) + + severity: str | None = Field( + default=None, + description="The severity, if any, this reference assigns to the belonging vulnerability. This " + 'string is supposed to be a qualitative rating like "LOW" or "HIGH".', + ) + + score: float | None = Field( + default=None, + description="The (base) score, if any, this reference assigns to the belonging vulnerability. " + "The meaning of this number depends on the scoring system.", + ) + + vector: str | None = Field( + default=None, + description="For CVSS, this is the full vector this reference assigns to the belonging " + "vulnerability, if any. While the vector usually contains the scoring system as a " + "prefix, that is not the case for CVSS v2. For EPSS, this is the percentile, which " + "provides a measure of probability relative to all other scores.", + ) + + @staticmethod + def get_qualitative_rating(scoring_system: str | None, score: float | None) -> Enum | None: + """ + Get the qualitative rating for the given scoring system and score. + + Returns the corresponding CVSS rating enum member, or None if the scoring system is unknown + or the score does not map to any rating. + """ + if scoring_system is None or score is None: + return None + + upper = scoring_system.upper() + + for rating_cls in (Cvss2Rating, Cvss3Rating, Cvss4Rating): + if any(upper.startswith(prefix) for prefix in rating_cls.prefixes()): + return rating_cls.from_score(score) + + return None diff --git a/tests/data/advisor/advisor_result.yml b/tests/data/advisor/advisor_result.yml new file mode 100644 index 0000000..e02808c --- /dev/null +++ b/tests/data/advisor/advisor_result.yml @@ -0,0 +1,29 @@ +advisor: + name: "VulnerableCode" + capabilities: + - "VULNERABILITIES" +summary: + start_time: "2024-06-01T10:00:00Z" + end_time: "2024-06-01T10:05:00Z" + issues: [] +vulnerabilities: + - id: "CVE-2024-1234" + summary: "A critical vulnerability in example package." + description: "Detailed description of CVE-2024-1234." + references: + - url: "https://nvd.nist.gov/vuln/detail/CVE-2024-1234" + scoring_system: "CVSS:3.1" + severity: "HIGH" + score: 8.5 + vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + - url: "https://example.com/CVE-2024-1234" + scoring_system: "CVSS2" + severity: "MEDIUM" + score: 5.0 + - id: "CVE-2024-5678" + references: + - url: "https://nvd.nist.gov/vuln/detail/CVE-2024-5678" + scoring_system: "CVSS:4.0" + severity: "CRITICAL" + score: 9.8 +defects: [] diff --git a/tests/data/advisor/advisor_result_defects.yml b/tests/data/advisor/advisor_result_defects.yml new file mode 100644 index 0000000..dc4e0b9 --- /dev/null +++ b/tests/data/advisor/advisor_result_defects.yml @@ -0,0 +1,20 @@ +advisor: + name: "OSV" + capabilities: + - "DEFECTS" + - "VULNERABILITIES" +summary: + start_time: "2024-06-01T10:00:00Z" + end_time: "2024-06-01T10:05:00Z" + issues: + - timestamp: "2024-06-01T10:02:00Z" + source: "OSV" + message: "Timeout while querying OSV API." + severity: 2 +vulnerabilities: [] +defects: + - id: "BUG-42" + url: "https://issues.example.com/BUG-42" + title: "Null pointer exception in parser" + state: "OPEN" + severity: "HIGH" diff --git a/tests/test_advisor_capability.py b/tests/test_advisor_capability.py new file mode 100644 index 0000000..4659c88 --- /dev/null +++ b/tests/test_advisor_capability.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +import pytest + +from ort.models.advisor_capability import AdvisorCapability + + +def test_capability_values(): + """Test that AdvisorCapability has the expected integer values.""" + if AdvisorCapability.DEFECTS != 1: + pytest.fail("DEFECTS should equal 1") + if AdvisorCapability.VULNERABILITIES != 2: + pytest.fail("VULNERABILITIES should equal 2") + + +def test_capability_from_int(): + """Test that AdvisorCapability can be created from integer values.""" + if AdvisorCapability(1) != AdvisorCapability.DEFECTS: + pytest.fail("AdvisorCapability(1) should be DEFECTS") + if AdvisorCapability(2) != AdvisorCapability.VULNERABILITIES: + pytest.fail("AdvisorCapability(2) should be VULNERABILITIES") + + +def test_capability_from_name(): + """Test that AdvisorCapability can be accessed by name.""" + if AdvisorCapability["DEFECTS"] != AdvisorCapability.DEFECTS: + pytest.fail("AdvisorCapability['DEFECTS'] should be DEFECTS") + if AdvisorCapability["VULNERABILITIES"] != AdvisorCapability.VULNERABILITIES: + pytest.fail("AdvisorCapability['VULNERABILITIES'] should be VULNERABILITIES") + + +def test_capability_invalid_value(): + """Test that an invalid integer value raises ValueError.""" + with pytest.raises(ValueError): + AdvisorCapability(99) + + +def test_capability_invalid_name(): + """Test that an invalid name raises KeyError.""" + with pytest.raises(KeyError): + AdvisorCapability["INVALID"] + + +def test_capability_members(): + """Test that only the expected members exist.""" + if set(AdvisorCapability) != {AdvisorCapability.DEFECTS, AdvisorCapability.VULNERABILITIES}: + pytest.fail("AdvisorCapability should have exactly DEFECTS and VULNERABILITIES members") diff --git a/tests/test_advisor_details.py b/tests/test_advisor_details.py new file mode 100644 index 0000000..9640ab9 --- /dev/null +++ b/tests/test_advisor_details.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +import pytest +from pydantic import ValidationError + +from ort.models.advisor_capability import AdvisorCapability +from ort.models.advisor_details import AdvisorDetails + + +def test_details_from_string_capabilities(): + """Test that AdvisorDetails converts string capabilities to enum values.""" + details = AdvisorDetails( + name="VulnerableCode", + capabilities={AdvisorCapability.VULNERABILITIES}, + ) + if details.name != "VulnerableCode": + pytest.fail(f"Expected name 'VulnerableCode', got '{details.name}'") + if details.capabilities != {AdvisorCapability.VULNERABILITIES}: + pytest.fail(f"Expected {{VULNERABILITIES}}, got {details.capabilities}") + + +def test_details_from_multiple_string_capabilities(): + """Test that AdvisorDetails converts multiple string capabilities.""" + details = AdvisorDetails( + name="OSV", + capabilities={AdvisorCapability.DEFECTS, AdvisorCapability.VULNERABILITIES}, + ) + if details.capabilities != {AdvisorCapability.DEFECTS, AdvisorCapability.VULNERABILITIES}: + pytest.fail(f"Expected {{DEFECTS, VULNERABILITIES}}, got {details.capabilities}") + + +def test_details_from_int_capabilities(): + """Test that AdvisorDetails accepts integer capability values.""" + details = AdvisorDetails( + name="TestAdvisor", + capabilities=[1, 2], # ty: ignore[invalid-argument-type] + ) + if details.capabilities != {AdvisorCapability.DEFECTS, AdvisorCapability.VULNERABILITIES}: + pytest.fail(f"Expected {{DEFECTS, VULNERABILITIES}}, got {details.capabilities}") + + +def test_details_from_mixed_capabilities(): + """Test that AdvisorDetails handles a mix of string and int capabilities.""" + details = AdvisorDetails( + name="TestAdvisor", + capabilities=["DEFECTS", 2], # ty: ignore[invalid-argument-type] + ) + if details.capabilities != {AdvisorCapability.DEFECTS, AdvisorCapability.VULNERABILITIES}: + pytest.fail(f"Expected {{DEFECTS, VULNERABILITIES}}, got {details.capabilities}") + + +def test_details_invalid_capability_string(): + """Test that an invalid capability string raises a ValidationError.""" + with pytest.raises(ValidationError): + AdvisorDetails( + name="TestAdvisor", + capabilities=["INVALID"], # ty: ignore[invalid-argument-type] + ) + + +def test_details_extra_field_forbidden(): + """Test that extra fields are rejected due to extra='forbid'.""" + with pytest.raises(ValidationError): + AdvisorDetails( + name="TestAdvisor", + capabilities={AdvisorCapability.VULNERABILITIES}, + unknown_field="value", # ty: ignore[unknown-argument] + ) + + +def test_details_missing_name(): + """Test that missing 'name' field raises a ValidationError.""" + with pytest.raises(ValidationError): + AdvisorDetails(capabilities={AdvisorCapability.VULNERABILITIES}) # ty: ignore[missing-argument] + + +def test_details_missing_capabilities(): + """Test that missing 'capabilities' field raises a ValidationError.""" + with pytest.raises(ValidationError): + AdvisorDetails(name="TestAdvisor") # ty: ignore[missing-argument] diff --git a/tests/test_advisor_result.py b/tests/test_advisor_result.py new file mode 100644 index 0000000..951a72c --- /dev/null +++ b/tests/test_advisor_result.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +from datetime import datetime, timezone + +import pytest +from pydantic import ValidationError + +from ort.models.advisor_capability import AdvisorCapability +from ort.models.advisor_details import AdvisorDetails +from ort.models.advisor_result import AdvisorResult +from ort.models.advisor_summary import AdvisorSummary +from tests.utils.load_yaml_config import load_yaml_config + + +def test_advisor_result_from_yaml(): + """Test loading a full AdvisorResult from YAML test data.""" + config_data = load_yaml_config("advisor_result.yml", "advisor") + + try: + result = AdvisorResult(**config_data) + except ValidationError as e: + pytest.fail(f"Failed to instantiate AdvisorResult: {e}") + + if result.advisor.name != "VulnerableCode": + pytest.fail(f"Expected advisor name 'VulnerableCode', got '{result.advisor.name}'") + if result.advisor.capabilities != {AdvisorCapability.VULNERABILITIES}: + pytest.fail(f"Expected {{VULNERABILITIES}}, got {result.advisor.capabilities}") + if len(result.vulnerabilities) != 2: + pytest.fail(f"Expected 2 vulnerabilities, got {len(result.vulnerabilities)}") + if result.vulnerabilities[0].id != "CVE-2024-1234": + pytest.fail(f"Expected first vuln id 'CVE-2024-1234', got '{result.vulnerabilities[0].id}'") + if len(result.vulnerabilities[0].references) != 2: + pytest.fail(f"Expected 2 references, got {len(result.vulnerabilities[0].references)}") + if result.vulnerabilities[0].references[0].scoring_system != "CVSS:3.1": + pytest.fail( + f"Expected scoring_system 'CVSS:3.1', got '{result.vulnerabilities[0].references[0].scoring_system}'" + ) + if result.vulnerabilities[0].references[0].score != 8.5: + pytest.fail(f"Expected score 8.5, got {result.vulnerabilities[0].references[0].score}") + if result.defects != []: + pytest.fail(f"Expected empty defects, got {result.defects}") + + +def test_advisor_result_with_defects_from_yaml(): + """Test loading an AdvisorResult with defects and issues from YAML.""" + config_data = load_yaml_config("advisor_result_defects.yml", "advisor") + + try: + result = AdvisorResult(**config_data) + except ValidationError as e: + pytest.fail(f"Failed to instantiate AdvisorResult: {e}") + + if result.advisor.name != "OSV": + pytest.fail(f"Expected advisor name 'OSV', got '{result.advisor.name}'") + if result.advisor.capabilities != {AdvisorCapability.DEFECTS, AdvisorCapability.VULNERABILITIES}: + pytest.fail(f"Expected {{DEFECTS, VULNERABILITIES}}, got {result.advisor.capabilities}") + if len(result.summary.issues) != 1: + pytest.fail(f"Expected 1 issue, got {len(result.summary.issues)}") + if result.summary.issues[0].source != "OSV": + pytest.fail(f"Expected issue source 'OSV', got '{result.summary.issues[0].source}'") + if len(result.defects) != 1: + pytest.fail(f"Expected 1 defect, got {len(result.defects)}") + if result.defects[0].id != "BUG-42": + pytest.fail(f"Expected defect id 'BUG-42', got '{result.defects[0].id}'") + if result.defects[0].title != "Null pointer exception in parser": + pytest.fail(f"Expected defect title 'Null pointer exception in parser', got '{result.defects[0].title}'") + if result.vulnerabilities != []: + pytest.fail(f"Expected empty vulnerabilities, got {result.vulnerabilities}") + + +def test_advisor_result_minimal(): + """Test creating a minimal AdvisorResult programmatically.""" + result = AdvisorResult( + advisor=AdvisorDetails( + name="TestAdvisor", + capabilities={AdvisorCapability.VULNERABILITIES}, + ), + summary=AdvisorSummary( + start_time=datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + end_time=datetime(2024, 1, 1, 0, 1, 0, tzinfo=timezone.utc), + ), + ) + if result.advisor.name != "TestAdvisor": + pytest.fail(f"Expected advisor name 'TestAdvisor', got '{result.advisor.name}'") + if result.vulnerabilities != []: + pytest.fail(f"Expected empty vulnerabilities, got {result.vulnerabilities}") + if result.defects != []: + pytest.fail(f"Expected empty defects, got {result.defects}") + + +def test_advisor_result_missing_advisor(): + """Test that missing advisor field raises ValidationError.""" + with pytest.raises(ValidationError): + AdvisorResult( + summary=AdvisorSummary( + start_time=datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + end_time=datetime(2024, 1, 1, 0, 1, 0, tzinfo=timezone.utc), + ), + ) # ty: ignore[missing-argument] + + +def test_advisor_result_missing_summary(): + """Test that missing summary field raises ValidationError.""" + with pytest.raises(ValidationError): + AdvisorResult( + advisor=AdvisorDetails( + name="TestAdvisor", + capabilities={AdvisorCapability.VULNERABILITIES}, + ), + ) # ty: ignore[missing-argument] + + +def test_advisor_summary_timestamps(): + """Test AdvisorSummary timestamp parsing.""" + summary = AdvisorSummary( + start_time=datetime(2024, 6, 1, 10, 0, 0, tzinfo=timezone.utc), + end_time=datetime(2024, 6, 1, 10, 5, 0, tzinfo=timezone.utc), + ) + if summary.start_time.year != 2024: + pytest.fail(f"Expected year 2024, got {summary.start_time.year}") + if summary.start_time.month != 6: + pytest.fail(f"Expected month 6, got {summary.start_time.month}") + if summary.end_time.minute != 5: + pytest.fail(f"Expected minute 5, got {summary.end_time.minute}") + if summary.issues != []: + pytest.fail(f"Expected empty issues, got {summary.issues}") diff --git a/tests/test_cvss_ratings.py b/tests/test_cvss_ratings.py new file mode 100644 index 0000000..fdc0a84 --- /dev/null +++ b/tests/test_cvss_ratings.py @@ -0,0 +1,177 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +import pytest + +from ort.models.vulnerabilities.cvss2_rating import Cvss2Rating +from ort.models.vulnerabilities.cvss3_rating import Cvss3Rating +from ort.models.vulnerabilities.cvss4_rating import Cvss4Rating + +# --- Cvss2Rating tests --- + + +class TestCvss2Rating: + def test_members(self): + """Test that Cvss2Rating has exactly the expected members.""" + if set(Cvss2Rating) != {Cvss2Rating.LOW, Cvss2Rating.MEDIUM, Cvss2Rating.HIGH}: + pytest.fail("Cvss2Rating should have exactly LOW, MEDIUM, HIGH members") + + def test_upper_bounds(self): + """Test that each member has the correct upper bound.""" + if Cvss2Rating.LOW.upper_bound != 4.0: + pytest.fail(f"LOW upper_bound should be 4.0, got {Cvss2Rating.LOW.upper_bound}") + if Cvss2Rating.MEDIUM.upper_bound != 7.0: + pytest.fail(f"MEDIUM upper_bound should be 7.0, got {Cvss2Rating.MEDIUM.upper_bound}") + if Cvss2Rating.HIGH.upper_bound != 10.0: + pytest.fail(f"HIGH upper_bound should be 10.0, got {Cvss2Rating.HIGH.upper_bound}") + + def test_prefixes(self): + """Test that the expected CVSS v2 prefixes are returned.""" + if Cvss2Rating.prefixes() != {"CVSS2", "CVSSV2", "CVSS_V2", "CVSS:2"}: + pytest.fail(f"Unexpected prefixes: {Cvss2Rating.prefixes()}") + + @pytest.mark.parametrize( + "score, expected", + [ + (0.0, Cvss2Rating.LOW), + (3.9, Cvss2Rating.LOW), + (4.0, Cvss2Rating.MEDIUM), + (6.9, Cvss2Rating.MEDIUM), + (7.0, Cvss2Rating.HIGH), + (10.0, Cvss2Rating.HIGH), + ], + ) + def test_from_score_valid(self, score, expected): + """Test valid scores map to the expected rating.""" + result = Cvss2Rating.from_score(score) + if result != expected: + pytest.fail(f"Score {score}: expected {expected}, got {result}") + + @pytest.mark.parametrize("score", [-0.1, -1.0, 10.1, 100.0]) + def test_from_score_out_of_range(self, score): + """Test out-of-range scores return None.""" + result = Cvss2Rating.from_score(score) + if result is not None: + pytest.fail(f"Score {score}: expected None, got {result}") + + +# --- Cvss3Rating tests --- + + +class TestCvss3Rating: + def test_members(self): + """Test that Cvss3Rating has exactly the expected members.""" + expected = { + Cvss3Rating.NONE, + Cvss3Rating.LOW, + Cvss3Rating.MEDIUM, + Cvss3Rating.HIGH, + Cvss3Rating.CRITICAL, + } + if set(Cvss3Rating) != expected: + pytest.fail("Cvss3Rating should have exactly NONE, LOW, MEDIUM, HIGH, CRITICAL members") + + def test_upper_bounds(self): + """Test that each member has the correct upper bound.""" + if Cvss3Rating.NONE.upper_bound != 0.0: + pytest.fail(f"NONE upper_bound should be 0.0, got {Cvss3Rating.NONE.upper_bound}") + if Cvss3Rating.LOW.upper_bound != 4.0: + pytest.fail(f"LOW upper_bound should be 4.0, got {Cvss3Rating.LOW.upper_bound}") + if Cvss3Rating.MEDIUM.upper_bound != 7.0: + pytest.fail(f"MEDIUM upper_bound should be 7.0, got {Cvss3Rating.MEDIUM.upper_bound}") + if Cvss3Rating.HIGH.upper_bound != 9.0: + pytest.fail(f"HIGH upper_bound should be 9.0, got {Cvss3Rating.HIGH.upper_bound}") + if Cvss3Rating.CRITICAL.upper_bound != 10.0: + pytest.fail(f"CRITICAL upper_bound should be 10.0, got {Cvss3Rating.CRITICAL.upper_bound}") + + def test_prefixes(self): + """Test that the expected CVSS v3 prefixes are returned.""" + if Cvss3Rating.prefixes() != {"CVSS3", "CVSSV3", "CVSS_V3", "CVSS:3"}: + pytest.fail(f"Unexpected prefixes: {Cvss3Rating.prefixes()}") + + @pytest.mark.parametrize( + "score, expected", + [ + (0.0, Cvss3Rating.LOW), + (3.9, Cvss3Rating.LOW), + (4.0, Cvss3Rating.MEDIUM), + (6.9, Cvss3Rating.MEDIUM), + (7.0, Cvss3Rating.HIGH), + (8.9, Cvss3Rating.HIGH), + (9.0, Cvss3Rating.CRITICAL), + (10.0, Cvss3Rating.CRITICAL), + ], + ) + def test_from_score_valid(self, score, expected): + """Test valid scores map to the expected rating.""" + result = Cvss3Rating.from_score(score) + if result != expected: + pytest.fail(f"Score {score}: expected {expected}, got {result}") + + @pytest.mark.parametrize("score", [-0.1, -1.0, 10.1, 100.0]) + def test_from_score_out_of_range(self, score): + """Test out-of-range scores return None.""" + result = Cvss3Rating.from_score(score) + if result is not None: + pytest.fail(f"Score {score}: expected None, got {result}") + + +# --- Cvss4Rating tests --- + + +class TestCvss4Rating: + def test_members(self): + """Test that Cvss4Rating has exactly the expected members.""" + expected = { + Cvss4Rating.NONE, + Cvss4Rating.LOW, + Cvss4Rating.MEDIUM, + Cvss4Rating.HIGH, + Cvss4Rating.CRITICAL, + } + if set(Cvss4Rating) != expected: + pytest.fail("Cvss4Rating should have exactly NONE, LOW, MEDIUM, HIGH, CRITICAL members") + + def test_upper_bounds(self): + """Test that each member has the correct upper bound.""" + if Cvss4Rating.NONE.upper_bound != 0.0: + pytest.fail(f"NONE upper_bound should be 0.0, got {Cvss4Rating.NONE.upper_bound}") + if Cvss4Rating.LOW.upper_bound != 4.0: + pytest.fail(f"LOW upper_bound should be 4.0, got {Cvss4Rating.LOW.upper_bound}") + if Cvss4Rating.MEDIUM.upper_bound != 7.0: + pytest.fail(f"MEDIUM upper_bound should be 7.0, got {Cvss4Rating.MEDIUM.upper_bound}") + if Cvss4Rating.HIGH.upper_bound != 9.0: + pytest.fail(f"HIGH upper_bound should be 9.0, got {Cvss4Rating.HIGH.upper_bound}") + if Cvss4Rating.CRITICAL.upper_bound != 10.0: + pytest.fail(f"CRITICAL upper_bound should be 10.0, got {Cvss4Rating.CRITICAL.upper_bound}") + + def test_prefixes(self): + """Test that the expected CVSS v4 prefixes are returned.""" + if Cvss4Rating.prefixes() != {"CVSS4", "CVSSV4", "CVSS_V4", "CVSS:4"}: + pytest.fail(f"Unexpected prefixes: {Cvss4Rating.prefixes()}") + + @pytest.mark.parametrize( + "score, expected", + [ + (0.0, Cvss4Rating.LOW), + (3.9, Cvss4Rating.LOW), + (4.0, Cvss4Rating.MEDIUM), + (6.9, Cvss4Rating.MEDIUM), + (7.0, Cvss4Rating.HIGH), + (8.9, Cvss4Rating.HIGH), + (9.0, Cvss4Rating.CRITICAL), + (10.0, Cvss4Rating.CRITICAL), + ], + ) + def test_from_score_valid(self, score, expected): + """Test valid scores map to the expected rating.""" + result = Cvss4Rating.from_score(score) + if result != expected: + pytest.fail(f"Score {score}: expected {expected}, got {result}") + + @pytest.mark.parametrize("score", [-0.1, -1.0, 10.1, 100.0]) + def test_from_score_out_of_range(self, score): + """Test out-of-range scores return None.""" + result = Cvss4Rating.from_score(score) + if result is not None: + pytest.fail(f"Score {score}: expected None, got {result}") diff --git a/tests/test_ort_repository_configuration.py b/tests/test_ort_repository_configuration.py index b42d065..9043c94 100644 --- a/tests/test_ort_repository_configuration.py +++ b/tests/test_ort_repository_configuration.py @@ -5,12 +5,12 @@ import pytest from ort.models.repository_configuration import ( - OrtRepositoryConfiguration, OrtRepositoryConfigurationIncludes, OrtRepositoryConfigurationIncludesPath, PathIncludeReason, + RepositoryConfiguration, ) -from tests.utils.load_yaml_config import load_yaml_config # type: ignore +from tests.utils.load_yaml_config import load_yaml_config def test_only_include_valid(): @@ -43,14 +43,17 @@ def test_only_include_valid(): ) ] ) - repo_config = OrtRepositoryConfiguration(includes=includes_model) + repo_config = RepositoryConfiguration(includes=includes_model) except Exception as e: pytest.fail(f"Failed to instantiate OrtRepositoryConfiguration: {e}") if not repo_config.includes or not getattr(repo_config.includes, "paths", None): pytest.fail("No path includes are provided.") else: - path_obj = repo_config.includes.paths[0] + paths = getattr(repo_config.includes, "paths", None) + if not paths or not isinstance(paths, list): + pytest.fail("No path includes are provided or 'paths' is not a list.") + path_obj = paths[0] if path_obj.pattern != "test/**": pytest.fail(f"Pattern mismatch: {path_obj.pattern}") if path_obj.reason != PathIncludeReason.source_of: diff --git a/tests/test_package_configuration.py b/tests/test_package_configuration.py index 5398b72..7d4716b 100644 --- a/tests/test_package_configuration.py +++ b/tests/test_package_configuration.py @@ -6,7 +6,7 @@ from pydantic import ValidationError from ort.models.config.package_configuration import PackageConfiguration -from tests.utils.load_yaml_config import load_yaml_config # type: ignore +from tests.utils.load_yaml_config import load_yaml_config def test_ort_docs_simple_package_configuration(): diff --git a/tests/test_package_curation.py b/tests/test_package_curation.py index ebeaa8b..87b9dc5 100644 --- a/tests/test_package_curation.py +++ b/tests/test_package_curation.py @@ -6,7 +6,7 @@ from pydantic import ValidationError from ort.models.config.curations import Curations -from tests.utils.load_yaml_config import load_yaml_config # type: ignore +from tests.utils.load_yaml_config import load_yaml_config def test_ort_docs_simple_curation_example(): diff --git a/tests/test_repository_analyzer_config.py b/tests/test_repository_analyzer_config.py index 0ad7e35..1addbd6 100644 --- a/tests/test_repository_analyzer_config.py +++ b/tests/test_repository_analyzer_config.py @@ -8,7 +8,7 @@ from pydantic import ValidationError from ort.models.config.repository_analyzer_configuration import RepositoryAnalyzerConfiguration -from tests.utils.load_yaml_config import load_yaml_config # type: ignore +from tests.utils.load_yaml_config import load_yaml_config def test_boolean_option_conversion(): diff --git a/tests/test_vulnerability_reference.py b/tests/test_vulnerability_reference.py new file mode 100644 index 0000000..8e0c9d7 --- /dev/null +++ b/tests/test_vulnerability_reference.py @@ -0,0 +1,215 @@ +# SPDX-FileCopyrightText: 2026 Helio Chissini de Castro +# SPDX-License-Identifier: MIT + +import pytest +from pydantic import ValidationError + +from ort.models.vulnerabilities.cvss2_rating import Cvss2Rating +from ort.models.vulnerabilities.cvss3_rating import Cvss3Rating +from ort.models.vulnerabilities.cvss4_rating import Cvss4Rating +from ort.models.vulnerabilities.vulnerability import Vulnerability +from ort.models.vulnerabilities.vulnerability_reference import VulnerabilityReference + + +class TestVulnerabilityReference: + def test_minimal_reference(self): + """Test creating a reference with only the required url field.""" + ref = VulnerabilityReference(url="https://nvd.nist.gov/vuln/detail/CVE-2024-1234") + if ref.url != "https://nvd.nist.gov/vuln/detail/CVE-2024-1234": + pytest.fail(f"Expected url to match, got '{ref.url}'") + if ref.scoring_system is not None: + pytest.fail(f"Expected scoring_system to be None, got '{ref.scoring_system}'") + if ref.severity is not None: + pytest.fail(f"Expected severity to be None, got '{ref.severity}'") + if ref.score is not None: + pytest.fail(f"Expected score to be None, got {ref.score}") + if ref.vector is not None: + pytest.fail(f"Expected vector to be None, got '{ref.vector}'") + + def test_full_reference(self): + """Test creating a reference with all fields populated.""" + ref = VulnerabilityReference( + url="https://nvd.nist.gov/vuln/detail/CVE-2024-1234", + scoring_system="CVSS:3.1", + severity="HIGH", + score=8.5, + vector="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + ) + if ref.scoring_system != "CVSS:3.1": + pytest.fail(f"Expected scoring_system 'CVSS:3.1', got '{ref.scoring_system}'") + if ref.severity != "HIGH": + pytest.fail(f"Expected severity 'HIGH', got '{ref.severity}'") + if ref.score != 8.5: + pytest.fail(f"Expected score 8.5, got {ref.score}") + if ref.vector != "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H": + pytest.fail(f"Expected vector to match, got '{ref.vector}'") + + def test_scoring_system_alias(self): + """Test that scoringSystem alias maps to scoring_system field.""" + ref = VulnerabilityReference( + url="https://example.com", + scoring_system="CVSS2", + ) + if ref.scoring_system != "CVSS2": + pytest.fail(f"Expected scoring_system 'CVSS2', got '{ref.scoring_system}'") + + def test_extra_field_forbidden(self): + """Test that extra fields are rejected.""" + with pytest.raises(ValidationError): + VulnerabilityReference( + url="https://example.com", + unknown="value", # ty: ignore[unknown-argument] + ) + + +class TestGetQualitativeRating: + @pytest.mark.parametrize( + "scoring_system, score, expected", + [ + ("CVSS2", 3.5, Cvss2Rating.LOW), + ("CVSSV2", 5.0, Cvss2Rating.MEDIUM), + ("CVSS_V2", 9.0, Cvss2Rating.HIGH), + ("CVSS:2", 10.0, Cvss2Rating.HIGH), + ], + ) + def test_cvss2_ratings(self, scoring_system, score, expected): + """Test that CVSS v2 scoring systems return correct ratings.""" + result = VulnerabilityReference.get_qualitative_rating(scoring_system, score) + if result != expected: + pytest.fail(f"{scoring_system} score {score}: expected {expected}, got {result}") + + @pytest.mark.parametrize( + "scoring_system, score, expected", + [ + ("CVSS3", 2.0, Cvss3Rating.LOW), + ("CVSSV3", 5.5, Cvss3Rating.MEDIUM), + ("CVSS_V3", 8.0, Cvss3Rating.HIGH), + ("CVSS:3", 9.5, Cvss3Rating.CRITICAL), + ("CVSS:3.1", 8.5, Cvss3Rating.HIGH), + ], + ) + def test_cvss3_ratings(self, scoring_system, score, expected): + """Test that CVSS v3 scoring systems return correct ratings.""" + result = VulnerabilityReference.get_qualitative_rating(scoring_system, score) + if result != expected: + pytest.fail(f"{scoring_system} score {score}: expected {expected}, got {result}") + + @pytest.mark.parametrize( + "scoring_system, score, expected", + [ + ("CVSS4", 3.0, Cvss4Rating.LOW), + ("CVSSV4", 5.5, Cvss4Rating.MEDIUM), + ("CVSS_V4", 8.0, Cvss4Rating.HIGH), + ("CVSS:4", 10.0, Cvss4Rating.CRITICAL), + ("CVSS:4.0", 9.8, Cvss4Rating.CRITICAL), + ], + ) + def test_cvss4_ratings(self, scoring_system, score, expected): + """Test that CVSS v4 scoring systems return correct ratings.""" + result = VulnerabilityReference.get_qualitative_rating(scoring_system, score) + if result != expected: + pytest.fail(f"{scoring_system} score {score}: expected {expected}, got {result}") + + def test_case_insensitive_scoring_system(self): + """Test that scoring system matching is case-insensitive.""" + result1 = VulnerabilityReference.get_qualitative_rating("cvss:3.1", 8.5) + if result1 != Cvss3Rating.HIGH: + pytest.fail(f"Expected Cvss3Rating.HIGH for 'cvss:3.1', got {result1}") + result2 = VulnerabilityReference.get_qualitative_rating("Cvss2", 3.5) + if result2 != Cvss2Rating.LOW: + pytest.fail(f"Expected Cvss2Rating.LOW for 'Cvss2', got {result2}") + + def test_none_scoring_system(self): + """Test that None scoring system returns None.""" + result = VulnerabilityReference.get_qualitative_rating(None, 5.0) + if result is not None: + pytest.fail(f"Expected None, got {result}") + + def test_none_score(self): + """Test that None score returns None.""" + result = VulnerabilityReference.get_qualitative_rating("CVSS:3.1", None) + if result is not None: + pytest.fail(f"Expected None, got {result}") + + def test_both_none(self): + """Test that both None returns None.""" + result = VulnerabilityReference.get_qualitative_rating(None, None) + if result is not None: + pytest.fail(f"Expected None, got {result}") + + def test_unknown_scoring_system(self): + """Test that an unknown scoring system returns None.""" + result1 = VulnerabilityReference.get_qualitative_rating("UNKNOWN", 5.0) + if result1 is not None: + pytest.fail(f"Expected None for 'UNKNOWN', got {result1}") + result2 = VulnerabilityReference.get_qualitative_rating("EPSS", 0.5) + if result2 is not None: + pytest.fail(f"Expected None for 'EPSS', got {result2}") + + def test_out_of_range_score(self): + """Test that out-of-range scores return None.""" + result1 = VulnerabilityReference.get_qualitative_rating("CVSS:3.1", -1.0) + if result1 is not None: + pytest.fail(f"Expected None for score -1.0, got {result1}") + result2 = VulnerabilityReference.get_qualitative_rating("CVSS:3.1", 10.1) + if result2 is not None: + pytest.fail(f"Expected None for score 10.1, got {result2}") + + +class TestVulnerability: + def test_vulnerability_with_references(self): + """Test creating a Vulnerability with VulnerabilityReference objects.""" + vuln = Vulnerability( + id="CVE-2024-1234", + summary="A critical vulnerability.", + description="Detailed description.", + references=[ + VulnerabilityReference( + url="https://nvd.nist.gov/vuln/detail/CVE-2024-1234", + scoring_system="CVSS:3.1", + severity="HIGH", + score=8.5, + ), + VulnerabilityReference( + url="https://example.com/CVE-2024-1234", + scoring_system="CVSS2", + severity="MEDIUM", + score=5.0, + ), + ], + ) + if vuln.id != "CVE-2024-1234": + pytest.fail(f"Expected id 'CVE-2024-1234', got '{vuln.id}'") + if len(vuln.references) != 2: + pytest.fail(f"Expected 2 references, got {len(vuln.references)}") + if vuln.references[0].scoring_system != "CVSS:3.1": + pytest.fail(f"Expected scoring_system 'CVSS:3.1', got '{vuln.references[0].scoring_system}'") + if vuln.references[1].score != 5.0: + pytest.fail(f"Expected score 5.0, got {vuln.references[1].score}") + + def test_vulnerability_minimal(self): + """Test creating a Vulnerability with only required fields.""" + vuln = Vulnerability( + id="CVE-2024-5678", + references=[ + VulnerabilityReference(url="https://example.com/vuln"), + ], + ) + if vuln.id != "CVE-2024-5678": + pytest.fail(f"Expected id 'CVE-2024-5678', got '{vuln.id}'") + if vuln.summary is not None: + pytest.fail(f"Expected summary to be None, got '{vuln.summary}'") + if vuln.description is not None: + pytest.fail(f"Expected description to be None, got '{vuln.description}'") + if len(vuln.references) != 1: + pytest.fail(f"Expected 1 reference, got {len(vuln.references)}") + + def test_vulnerability_missing_id(self): + """Test that missing id raises ValidationError.""" + with pytest.raises(ValidationError): + Vulnerability(references=[VulnerabilityReference(url="https://example.com")]) + + def test_vulnerability_missing_references(self): + """Test that missing references raises ValidationError.""" + with pytest.raises(ValidationError): + Vulnerability(id="CVE-2024-1234") diff --git a/tests/utils/load_yaml_config.py b/tests/utils/load_yaml_config.py index 7a83a06..727b816 100644 --- a/tests/utils/load_yaml_config.py +++ b/tests/utils/load_yaml_config.py @@ -10,7 +10,7 @@ DATA_CONFIG_DIR = Path(__file__).parent.parent / "data" -def load_yaml_config(filename: str, data_dir: Path | None = None) -> Any: +def load_yaml_config(filename: str, data_dir: Path | str | None = None) -> Any: """ Load a YAML configuration file from the REPO_CONFIG_DIR directory. diff --git a/uv.lock b/uv.lock index 3f478ba..2f1f5ab 100644 --- a/uv.lock +++ b/uv.lock @@ -87,15 +87,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] -[[package]] -name = "cfgv" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, -] - [[package]] name = "click" version = "8.3.1" @@ -119,7 +110,7 @@ wheels = [ [[package]] name = "datamodel-code-generator" -version = "0.53.0" +version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, @@ -133,9 +124,9 @@ dependencies = [ { name = "pyyaml" }, { name = "tomli", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/65/3802abca0291263862a16e032e984e61e4d0d30a344d9be97815721d64ff/datamodel_code_generator-0.53.0.tar.gz", hash = "sha256:af46b57ad78e6435873132c52843ef0ec7b768a591d3b9917d3409dfc1ab1c90", size = 809949, upload-time = "2026-01-12T18:14:05.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/43/2640cd5293fb5430528166908d6439cf3321bc1c54de5fe58ef100b143a1/datamodel_code_generator-0.54.0.tar.gz", hash = "sha256:2b183598d049e265146a8224c35d1bb96a80a641ea8ecd2a82e6a0e97b56da6b", size = 824327, upload-time = "2026-02-14T16:19:05.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/43/5dbb6fe09842e10062f94016ccb48c9613f2443253866de3d7b815713b4d/datamodel_code_generator-0.53.0-py3-none-any.whl", hash = "sha256:d1cc2abe79f99b8208c363f5f4b603c29290327ff4e3219a08c0fff45f42aff4", size = 258912, upload-time = "2026-01-12T18:14:02.737Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d9/fd646ea4ae48e374817b2750f5e678a5bf6e10d8924f09cf4cce86a81607/datamodel_code_generator-0.54.0-py3-none-any.whl", hash = "sha256:3156df7a7e8fa5a7c9a6d50836e5ba5abe0532f6b71eee6d73a0c8e1fb5b7e47", size = 261838, upload-time = "2026-02-14T16:19:03.298Z" }, ] [package.optional-dependencies] @@ -143,15 +134,6 @@ http = [ { name = "httpx" }, ] -[[package]] -name = "distlib" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, -] - [[package]] name = "exceptiongroup" version = "1.3.1" @@ -164,15 +146,6 @@ 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" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, -] - [[package]] name = "genson" version = "1.3.0" @@ -219,15 +192,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[[package]] -name = "identify" -version = "2.6.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, -] - [[package]] name = "idna" version = "3.11" @@ -404,15 +368,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - [[package]] name = "packaging" version = "26.0" @@ -424,20 +379,20 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.3" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] @@ -449,31 +404,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "pre-commit" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, -] - -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, -] - [[package]] name = "pydantic" version = "2.12.5" @@ -616,22 +546,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyrefly" -version = "0.49.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b4/19/8ad522587672c6bb013e284ee8a326136f6511c74784141f3fd550b99aee/pyrefly-0.49.0.tar.gz", hash = "sha256:d4e9a978d55253d2cd24c0354bd4cf087026d07bd374388c2ae12a3bc26f93fc", size = 4822135, upload-time = "2026-01-20T15:13:48.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/47/8c34be1fd5fb3ca74608a71dfece40c4b9d382a8899db8418be9b326ba3f/pyrefly-0.49.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1cd5516ddab7c745e195fe1470629251962498482025bf2a9a9d53d5bde73729", size = 11644108, upload-time = "2026-01-20T15:13:25.358Z" }, - { url = "https://files.pythonhosted.org/packages/57/01/f492c92b4df963dbfda8d8e1cf57477704df8cdecf907568580af60193fe/pyrefly-0.49.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a998a37dc1465a648c03076545080a8bd2a421c67cac27686eca43244e8ac69", size = 11246465, upload-time = "2026-01-20T15:13:27.845Z" }, - { url = "https://files.pythonhosted.org/packages/d1/0b/89da00960e9c43ae7aa5f50886e9f87457137c444e513c00b714fdc6ba1e/pyrefly-0.49.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a96b1452fa61d7db6d5ae6b6297f50ba8c006ba7ce420233ebd33eaf95d04cfd", size = 31723528, upload-time = "2026-01-20T15:13:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/f7/69/43a2a1a6bc00037879643d7d5257215fea1988dd2ef3168b5fe3cd55dcf0/pyrefly-0.49.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97f1b5fb1be6f8f4868fe40e7ebeed055c8483012212267e182d58a8e50723e7", size = 33924099, upload-time = "2026-01-20T15:13:35.056Z" }, - { url = "https://files.pythonhosted.org/packages/f4/df/e475cd37d40221571e25465f0a39dd14123b8a3498f103e39e5938a2645f/pyrefly-0.49.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7ee11eefd1d551629ce1b25888814dbf758aac1a10279537d9425bc53f2d41c", size = 35026928, upload-time = "2026-01-20T15:13:38.403Z" }, - { url = "https://files.pythonhosted.org/packages/54/e2/fe9588b2cb4685c410ebf106bf1d28c66ed2727a5eeeabcfb51fec714143/pyrefly-0.49.0-py3-none-win32.whl", hash = "sha256:6196cb9b20ee977f64fa1fe87e06d3f7a222c5155031d21139fc60464a7a4b9c", size = 10675311, upload-time = "2026-01-20T15:13:40.99Z" }, - { url = "https://files.pythonhosted.org/packages/1a/dc/65fba26966bc2d9a9cbef620ef2a957f72bf3551822d6c250e3d36c2d0ee/pyrefly-0.49.0-py3-none-win_amd64.whl", hash = "sha256:15333b5550fd32a8f9a971ad124714d75f1906a67e48033dcc203258525bc7fd", size = 11418250, upload-time = "2026-01-20T15:13:43.321Z" }, - { url = "https://files.pythonhosted.org/packages/54/3c/9b0af11cbbfd57c5487af2d5d7322c30e7d73179171e1ffa4dda758dd286/pyrefly-0.49.0-py3-none-win_arm64.whl", hash = "sha256:4a57eebced37836791b681626a4be004ebd27221bc208f8200e1e2ca8a8b9510", size = 10962081, upload-time = "2026-01-20T15:13:45.82Z" }, -] - [[package]] name = "pytest" version = "9.0.2" @@ -652,7 +566,7 @@ wheels = [ [[package]] name = "python-ort" -version = "0.5.0" +version = "0.6.0" source = { editable = "." } dependencies = [ { name = "pydantic" }, @@ -661,12 +575,10 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "datamodel-code-generator", extra = ["http"] }, - { name = "pre-commit" }, - { name = "pycodestyle" }, - { name = "pyrefly" }, { name = "pytest" }, { name = "rich" }, { name = "ruff" }, + { name = "ty" }, { name = "types-pyyaml" }, ] @@ -675,53 +587,51 @@ requires-dist = [{ name = "pydantic", specifier = ">=2.12.5" }] [package.metadata.requires-dev] dev = [ - { name = "datamodel-code-generator", extras = ["http"], specifier = ">=0.53.0" }, - { name = "pre-commit", specifier = ">=4.5.1" }, - { name = "pycodestyle", specifier = ">=2.14.0" }, - { name = "pyrefly", specifier = ">=0.49.0" }, + { name = "datamodel-code-generator", extras = ["http"], specifier = ">=0.54.0" }, { name = "pytest", specifier = ">=9.0.2" }, - { name = "rich", specifier = ">=14.2.0" }, - { name = "ruff", specifier = ">=0.14.14" }, + { name = "rich", specifier = ">=14.3.2" }, + { name = "ruff", specifier = ">=0.15.1" }, + { name = "ty", specifier = ">=0.0.17" }, { name = "types-pyyaml", specifier = ">=6.0.12.20250915" }, ] [[package]] name = "pytokens" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/16/4b9cfd90d55e66ffdb277d7ebe3bc25250c2311336ec3fc73b2673c794d5/pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d", size = 15039, upload-time = "2026-01-19T07:59:50.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/c5/c20818fef16c4ab5f9fd7bad699268ba21bf24f655711df4e33bb7a9ab47/pytokens-0.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:af0c3166aea367a9e755a283171befb92dd3043858b94ae9b3b7efbe9def26a3", size = 160682, upload-time = "2026-01-19T07:58:51.583Z" }, - { url = "https://files.pythonhosted.org/packages/46/c4/ad03e4abe05c6af57c4d7f8f031fafe80f0074796d09ab5a73bf2fac895f/pytokens-0.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae524ed14ca459932cbf51d74325bea643701ba8a8b0cc2d10f7cd4b3e2b63", size = 245748, upload-time = "2026-01-19T07:58:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b9/4a7ee0a692603b16d8fdfbc5c44e0f6910d45eec6b2c2188daa4670f179d/pytokens-0.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e95cb158c44d642ed62f555bf8136bbe780dbd64d2fb0b9169e11ffb944664c3", size = 258671, upload-time = "2026-01-19T07:58:55.667Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a3/02bb29dc4985fb8d759d9c96f189c3a828e74f0879fdb843e9fb7a1db637/pytokens-0.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df58d44630eaf25f587540e94bdf1fc50b4e6d5f212c786de0fb024bfcb8753a", size = 261749, upload-time = "2026-01-19T07:58:57.442Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/9a8bdcc5444d85d4dba4aa1b530d81af3edc4a9ab76bf1d53ea8bfe8479d/pytokens-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55efcc36f9a2e0e930cfba0ce7f83445306b02f8326745585ed5551864eba73a", size = 102805, upload-time = "2026-01-19T07:58:59.068Z" }, - { url = "https://files.pythonhosted.org/packages/b4/05/3196399a353dd4cd99138a88f662810979ee2f1a1cdb0b417cb2f4507836/pytokens-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92eb3ef88f27c22dc9dbab966ace4d61f6826e02ba04dac8e2d65ea31df56c8e", size = 160075, upload-time = "2026-01-19T07:59:00.316Z" }, - { url = "https://files.pythonhosted.org/packages/28/1d/c8fc4ed0a1c4f660391b201cda00b1d5bbcc00e2998e8bcd48b15eefd708/pytokens-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4b77858a680635ee9904306f54b0ee4781effb89e211ba0a773d76539537165", size = 247318, upload-time = "2026-01-19T07:59:01.636Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0e/53e55ba01f3e858d229cd84b02481542f42ba59050483a78bf2447ee1af7/pytokens-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25cacc20c2ad90acb56f3739d87905473c54ca1fa5967ffcd675463fe965865e", size = 259752, upload-time = "2026-01-19T07:59:04.229Z" }, - { url = "https://files.pythonhosted.org/packages/dc/56/2d930d7f899e3f21868ca6e8ec739ac31e8fc532f66e09cbe45d3df0a84f/pytokens-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fab535ebc9079e4db35cd63cb401901c7ce8720a9834f9ad44b9eb4e0f1d4", size = 262842, upload-time = "2026-01-19T07:59:06.14Z" }, - { url = "https://files.pythonhosted.org/packages/42/dd/4e7e6920d23deffaf66e6f40d45f7610dcbc132ca5d90ab4faccef22f624/pytokens-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d0f568d7e82b7e96be56d03b5081de40e43c904eb6492bf09aaca47cd55f35b", size = 102620, upload-time = "2026-01-19T07:59:07.839Z" }, - { url = "https://files.pythonhosted.org/packages/3d/65/65460ebbfefd0bc1b160457904370d44f269e6e4582e0a9b6cba7c267b04/pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a", size = 159864, upload-time = "2026-01-19T07:59:08.908Z" }, - { url = "https://files.pythonhosted.org/packages/25/70/a46669ec55876c392036b4da9808b5c3b1c5870bbca3d4cc923bf68bdbc1/pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1", size = 254448, upload-time = "2026-01-19T07:59:10.594Z" }, - { url = "https://files.pythonhosted.org/packages/62/0b/c486fc61299c2fc3b7f88ee4e115d4c8b6ffd1a7f88dc94b398b5b1bc4b8/pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd", size = 268863, upload-time = "2026-01-19T07:59:12.31Z" }, - { url = "https://files.pythonhosted.org/packages/79/92/b036af846707d25feaff7cafbd5280f1bd6a1034c16bb06a7c910209c1ab/pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544", size = 267181, upload-time = "2026-01-19T07:59:13.856Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c0/6d011fc00fefa74ce34816c84a923d2dd7c46b8dbc6ee52d13419786834c/pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae", size = 102814, upload-time = "2026-01-19T07:59:15.288Z" }, - { url = "https://files.pythonhosted.org/packages/98/63/627b7e71d557383da5a97f473ad50f8d9c2c1f55c7d3c2531a120c796f6e/pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6", size = 159744, upload-time = "2026-01-19T07:59:16.88Z" }, - { url = "https://files.pythonhosted.org/packages/28/d7/16f434c37ec3824eba6bcb6e798e5381a8dc83af7a1eda0f95c16fe3ade5/pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb", size = 253207, upload-time = "2026-01-19T07:59:18.069Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/04102856b9527701ae57d74a6393d1aca5bad18a1b1ca48ccffb3c93b392/pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce", size = 267452, upload-time = "2026-01-19T07:59:19.328Z" }, - { url = "https://files.pythonhosted.org/packages/0e/ef/0936eb472b89ab2d2c2c24bb81c50417e803fa89c731930d9fb01176fe9f/pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753", size = 265965, upload-time = "2026-01-19T07:59:20.613Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f5/64f3d6f7df4a9e92ebda35ee85061f6260e16eac82df9396020eebbca775/pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e", size = 102813, upload-time = "2026-01-19T07:59:22.012Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f1/d07e6209f18ef378fc2ae9dee8d1dfe91fd2447c2e2dbfa32867b6dd30cf/pytokens-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0d7374c917197106d3c4761374718bc55ea2e9ac0fb94171588ef5840ee1f016", size = 159968, upload-time = "2026-01-19T07:59:23.07Z" }, - { url = "https://files.pythonhosted.org/packages/0a/73/0eb111400abd382a04f253b269819db9fcc748aa40748441cebdcb6d068f/pytokens-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd3fa1caf9e47a72ee134a29ca6b5bea84712724bba165d6628baa190c6ea5b", size = 253373, upload-time = "2026-01-19T07:59:24.381Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8d/9e4e2fdb5bcaba679e54afcc304e9f13f488eb4d626e6b613f9553e03dbd/pytokens-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6986576b7b07fe9791854caa5347923005a80b079d45b63b0be70d50cce5f1", size = 267024, upload-time = "2026-01-19T07:59:25.74Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b7/e0a370321af2deb772cff14ff337e1140d1eac2c29a8876bfee995f486f0/pytokens-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9940f7c2e2f54fb1cb5fe17d0803c54da7a2bf62222704eb4217433664a186a7", size = 270912, upload-time = "2026-01-19T07:59:27.072Z" }, - { url = "https://files.pythonhosted.org/packages/7c/54/4348f916c440d4c3e68b53b4ed0e66b292d119e799fa07afa159566dcc86/pytokens-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:54691cf8f299e7efabcc25adb4ce715d3cef1491e1c930eaf555182f898ef66a", size = 103836, upload-time = "2026-01-19T07:59:28.112Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f8/a693c0cfa9c783a2a8c4500b7b2a8bab420f8ca4f2d496153226bf1c12e3/pytokens-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94ff5db97a0d3cd7248a5b07ba2167bd3edc1db92f76c6db00137bbaf068ddf8", size = 167643, upload-time = "2026-01-19T07:59:29.292Z" }, - { url = "https://files.pythonhosted.org/packages/c0/dd/a64eb1e9f3ec277b69b33ef1b40ffbcc8f0a3bafcde120997efc7bdefebf/pytokens-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0dd6261cd9cc95fae1227b1b6ebee023a5fd4a4b6330b071c73a516f5f59b63", size = 289553, upload-time = "2026-01-19T07:59:30.537Z" }, - { url = "https://files.pythonhosted.org/packages/df/22/06c1079d93dbc3bca5d013e1795f3d8b9ed6c87290acd6913c1c526a6bb2/pytokens-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdca8159df407dbd669145af4171a0d967006e0be25f3b520896bc7068f02c4", size = 302490, upload-time = "2026-01-19T07:59:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/8d/de/a6f5e43115b4fbf4b93aa87d6c83c79932cdb084f9711daae04549e1e4ad/pytokens-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b5770abeb2a24347380a1164a558f0ebe06e98aedbd54c45f7929527a5fb26e", size = 305652, upload-time = "2026-01-19T07:59:33.685Z" }, - { url = "https://files.pythonhosted.org/packages/ab/3d/c136e057cb622e36e0c3ff7a8aaa19ff9720050c4078235691da885fe6ee/pytokens-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:74500d72c561dad14c037a9e86a657afd63e277dd5a3bb7570932ab7a3b12551", size = 115472, upload-time = "2026-01-19T07:59:34.734Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3c/6941a82f4f130af6e1c68c076b6789069ef10c04559bd4733650f902fd3b/pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068", size = 13224, upload-time = "2026-01-19T07:59:49.822Z" }, +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] [[package]] @@ -790,41 +700,40 @@ wheels = [ [[package]] name = "rich" -version = "14.3.1" +version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] name = "ruff" -version = "0.14.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, - { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, - { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, - { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]] @@ -881,16 +790,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] +[[package]] +name = "ty" +version = "0.0.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/f4c322d9cded56edc016b1092c14b95cf58c8a33b4787316ea752bb9418e/ty-0.0.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb2dbd8acd5c5a55f4af0d479523e7c7265a88542efe73ed3d696eb1ba7b6454", size = 10051977, upload-time = "2026-02-13T13:26:57.741Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/80/ef/22f3ed401520afac90dbdf1f9b8b7755d85b0d5c35c1cb35cf5bd11b59c2/ty-0.0.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6f5b1aba97db9af86517b911674b02f5bc310750485dc47603a105bd0e83ddd", size = 10533623, upload-time = "2026-02-13T13:26:31.449Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/06737bb80aa1a9103b8651d2eb691a7e53f1ed54111152be25f4a02745db/ty-0.0.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8b11f1da7859e0ad69e84b3c5ef9a7b055ceed376a432fad44231bdfc48061c2", size = 10231140, upload-time = "2026-02-13T13:27:10.844Z" }, + { url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/2663984ac11de6d78f74432b8b14ba64d170b45194312852b7543cf7fd56/ty-0.0.17-py3-none-win32.whl", hash = "sha256:305b6ed150b2740d00a817b193373d21f0767e10f94ac47abfc3b2e5a5aec809", size = 9672932, upload-time = "2026-02-13T13:27:08.522Z" }, + { url = "https://files.pythonhosted.org/packages/de/b5/39be78f30b31ee9f5a585969930c7248354db90494ff5e3d0756560fb731/ty-0.0.17-py3-none-win_amd64.whl", hash = "sha256:531828267527aee7a63e972f54e5eee21d9281b72baf18e5c2850c6b862add83", size = 10542138, upload-time = "2026-02-13T13:27:17.084Z" }, + { url = "https://files.pythonhosted.org/packages/40/b7/f875c729c5d0079640c75bad2c7e5d43edc90f16ba242f28a11966df8f65/ty-0.0.17-py3-none-win_arm64.whl", hash = "sha256:de9810234c0c8d75073457e10a84825b9cd72e6629826b7f01c7a0b266ae25b1", size = 10023068, upload-time = "2026-02-13T13:26:39.637Z" }, +] + [[package]] name = "typeguard" -version = "4.4.4" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/68/71c1a15b5f65f40e91b65da23b8224dad41349894535a97f63a52e462196/typeguard-4.4.4.tar.gz", hash = "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", size = 75203, upload-time = "2025-06-18T09:56:07.624Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/ec/adfe3dd6b5f7c5fc0b3cecdf6f893f1756dbd23cf749cd1ae49db069414f/typeguard-4.5.0.tar.gz", hash = "sha256:749bea21cdb2553e12831bc29f1eae980b22c7de8331ab67ae7db9e85470b5a7", size = 79993, upload-time = "2026-02-15T00:24:25.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e", size = 34874, upload-time = "2025-06-18T09:56:05.999Z" }, + { url = "https://files.pythonhosted.org/packages/d4/35/7541d1f046491fac8ce05d543d3f0de0af02086ad864dd3a23535ec703b9/typeguard-4.5.0-py3-none-any.whl", hash = "sha256:cfda388fc88a9ce42a41890900d6f31ee124bea9b73bb84701a32438e92165c3", size = 36724, upload-time = "2026-02-15T00:24:23.581Z" }, ] [[package]] @@ -922,18 +855,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] - -[[package]] -name = "virtualenv" -version = "20.36.1" -source = { registry = "https://pypi.org/simple" } -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 = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, -]