This is an automated email from the ASF dual-hosted git repository.
arm pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/arm by this push:
new 95a23912 #901 - add support for XML in sbom tooling and conversion
from XML to JSON
95a23912 is described below
commit 95a2391291848073b62ea904143db4a453545411
Author: Alastair McFarlane <[email protected]>
AuthorDate: Wed Mar 25 17:51:46 2026 +0000
#901 - add support for XML in sbom tooling and conversion from XML to JSON
---
.pre-commit-config.yaml | 4 +-
atr/analysis.py | 1 +
atr/get/compose.py | 4 +-
atr/get/draft.py | 13 ++
atr/models/results.py | 11 ++
atr/models/sql.py | 1 +
atr/post/draft.py | 76 +++++++++-
atr/sbom/cli.py | 29 +++-
atr/sbom/conformance.py | 30 ++--
atr/sbom/constants/licenses.py | 1 +
atr/sbom/cyclonedx.py | 25 ++--
atr/sbom/licenses.py | 22 ++-
atr/sbom/models/__init__.py | 4 +-
atr/sbom/models/bom.py | 100 -------------
atr/sbom/models/bundle.py | 10 +-
atr/sbom/osv.py | 36 ++---
atr/sbom/tool.py | 24 +--
atr/sbom/utilities.py | 60 ++++++--
atr/storage/writers/sbom.py | 27 ++++
atr/tasks/__init__.py | 2 +
atr/tasks/sbom.py | 235 ++++++++++++++++++-----------
atr/templates/draft-tools.html | 8 +
pip-audit.requirements | 18 +--
pyproject.toml | 3 +
typestubs/cyclonedx/model/bom.pyi | 44 ++++++
typestubs/cyclonedx/model/component.pyi | 48 ++++++
typestubs/cyclonedx/output/__init__.pyi | 22 +++
typestubs/py_serializable/__init__.pyi | 94 ++++++++++++
uv.lock | 252 ++++++++++++++++----------------
29 files changed, 793 insertions(+), 411 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6d4690b5..a0a67f03 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -61,7 +61,7 @@ repos:
hooks:
- id: shellcheck
- repo: https://github.com/rtts/djhtml
- rev: 3.0.10
+ rev: 3.0.11
hooks:
- id: djhtml
files: .*/.*\.html$
@@ -84,7 +84,7 @@ repos:
# - --profile=jinja
# - --reformat
- repo: https://github.com/thibaudcolas/pre-commit-stylelint
- rev: v17.5.0
+ rev: v17.6.0
hooks:
- id: stylelint
additional_dependencies: ['[email protected]',
'[email protected]']
diff --git a/atr/analysis.py b/atr/analysis.py
index 8ee7d7d2..2ab5ad13 100755
--- a/atr/analysis.py
+++ b/atr/analysis.py
@@ -134,6 +134,7 @@ SKIPPABLE_SUFFIXES: Final[list[str]] = [
STANDALONE_METADATA_SUFFIXES: Final[frozenset[str]] = frozenset(
{
".cdx.json",
+ ".cdx.xml",
}
)
diff --git a/atr/get/compose.py b/atr/get/compose.py
index 906ba499..6cb7ce0a 100644
--- a/atr/get/compose.py
+++ b/atr/get/compose.py
@@ -47,7 +47,7 @@ if TYPE_CHECKING:
@get.typed
async def selected(
session: web.Committer,
- _compose: Literal["compose"],
+ _compose: Literal['compose'],
project_key: safe.ProjectKey,
version_key: safe.VersionKey,
) -> web.WerkzeugResponse | str:
@@ -62,7 +62,7 @@ async def selected(
version=str(version_key),
_committee=True,
_project_release_policy=True,
- ).demand(base.ASFQuartException("Release does not exist",
errorcode=404))
+ ).demand(base.ASFQuartException('Release does not exist',
errorcode=404))
if release.phase != sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
return await mapping.release_as_redirect(session, release)
diff --git a/atr/get/draft.py b/atr/get/draft.py
index 538915db..12d6c70c 100644
--- a/atr/get/draft.py
+++ b/atr/get/draft.py
@@ -90,6 +90,18 @@ async def tools(
submit_classes="btn-outline-secondary",
empty=True,
)
+ sbom_convert_form = form.render(
+ model_cls=form.Empty,
+ action=util.as_url(
+ post.draft.sbomconvert,
+ project_key=str(project_key),
+ version_key=str(version_key),
+ file_path=str(file_path),
+ ),
+ submit_label="Convert XML SBOM (.cdx.xml)",
+ submit_classes="btn-outline-secondary",
+ empty=True,
+ )
return await template.render(
"draft-tools.html",
@@ -102,4 +114,5 @@ async def tools(
format_file_size=util.format_file_size,
sha512_form=sha512_form,
sbom_form=sbom_form,
+ sbom_convert_form=sbom_convert_form,
)
diff --git a/atr/models/results.py b/atr/models/results.py
index 99f145f0..cd04e226 100644
--- a/atr/models/results.py
+++ b/atr/models/results.py
@@ -163,6 +163,16 @@ class SBOMAugment(schema.Strict):
)
+class SBOMConvert(schema.Strict):
+ kind: Literal["sbom_convert"] = schema.Field(alias="kind")
+ path: str = schema.description("The path to the converted SBOM file")
+ bom_version: int | None = schema.Field(
+ default=None,
+ strict=False,
+ description="BOM Version produced by the convert task",
+ )
+
+
class SBOMQsScore(schema.Strict):
kind: Literal["sbom_qs_score"] = schema.Field(alias="kind")
project_key: safe.ProjectKey = schema.description("Project name")
@@ -241,6 +251,7 @@ Results = Annotated[
| MessageSend
| MetadataUpdate
| SBOMAugment
+ | SBOMConvert
| SBOMGenerateCycloneDX
| SBOMOSVScan
| SBOMQsScore
diff --git a/atr/models/sql.py b/atr/models/sql.py
index 97f34564..09c711fb 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -214,6 +214,7 @@ class TaskType(enum.StrEnum):
QUARANTINE_VALIDATE = "quarantine_validate"
RAT_CHECK = "rat_check"
SBOM_AUGMENT = "sbom_augment"
+ SBOM_CONVERT = "sbom_convert"
SBOM_GENERATE_CYCLONEDX = "sbom_generate_cyclonedx"
SBOM_OSV_SCAN = "sbom_osv_scan"
SBOM_QS_SCORE = "sbom_qs_score"
diff --git a/atr/post/draft.py b/atr/post/draft.py
index 3666679f..ebfd9ec3 100644
--- a/atr/post/draft.py
+++ b/atr/post/draft.py
@@ -253,7 +253,7 @@ async def sbomgen(
or rel_path.name.endswith(".jar")
):
raise base.ASFQuartException(
- f"SBOM generation requires .tar.gz, .tgz, .zip or .jar files.
Received: {file_path!s}", errorcode=400
+ f"SBOM generation requires .tar.gz, .tgz, .zip or .jar files.
Received: {rel_path.name}", errorcode=400
)
try:
@@ -309,3 +309,77 @@ async def sbomgen(
project_key=str(project_key),
version_key=str(version_key),
)
+
+
[email protected]
+async def sbomconvert(
+ session: web.Committer,
+ _draft_sbomconvert: Literal["draft/sbomconvert"],
+ project_key: safe.ProjectKey,
+ version_key: safe.VersionKey,
+ file_path: safe.RelPath,
+ empty_form: form.Empty,
+) -> web.WerkzeugResponse:
+ """
+ URL: /draft/sbomconvert/<project_key>/<version_key>/<file_path>
+ Convert an XML CycloneDX SBOM file into JSON, creating a new revision.
+ """
+ rel_path = file_path.as_path()
+
+ # Check that the file is a .cdx.xml file before continuing
+ if not rel_path.name.endswith(".cdx.xml"):
+ raise base.ASFQuartException(f"SBOM converter requires .cdx.xml file.
Received: {rel_path.name}", errorcode=400)
+
+ try:
+ description = "SBOM conversion through web interface"
+ async with storage.write(session) as write:
+ wacp = await write.as_project_committee_participant(project_key)
+
+ async def modify(path: pathlib.Path, old_rev: sql.Revision | None)
-> None:
+ path_in_new_revision = path / rel_path
+ sbom_path_rel = rel_path.with_suffix(".cdx.json").name
+ sbom_path_in_new_revision = path / rel_path.parent /
sbom_path_rel
+
+ # Check that the source file exists in the new revision
+ if not await aiofiles.os.path.exists(path_in_new_revision):
+ log.error(f"Source file {rel_path} not found in new
revision for SBOM generation.")
+ raise web.FlashError("Source artifact file not found in
the new revision.")
+
+ # Check that the SBOM file does not already exist in the new
revision
+ if await aiofiles.os.path.exists(sbom_path_in_new_revision):
+ raise base.ASFQuartException("SBOM file already exists",
errorcode=400)
+
+ # This shouldn't happen as we need a revision to kick the task
off from
+ if old_rev is None:
+ raise web.FlashError("Internal error: Revision not found")
+
+ # Create and queue the task, using paths within the new
revision
+ sbom_task = await wacp.sbom.convert_cyclonedx(
+ project_key,
+ version_key,
+ old_rev.safe_number,
+ str(path_in_new_revision),
+ str(sbom_path_in_new_revision),
+ )
+ success = await interaction.wait_for_task(sbom_task)
+ if not success:
+ raise web.FlashError("Internal error: SBOM conversion
timed out")
+
+ result = await wacp.revision.create_revision_with_quarantine(
+ project_key, version_key, session.uid,
description=description, modify=modify
+ )
+
+ except Exception as e:
+ log.exception("Error generating SBOM:")
+ await quart.flash(f"Error generating SBOM: {e!s}", "error")
+ return await session.redirect(get.compose.selected,
project_key=str(project_key), version_key=str(version_key))
+
+ success = f"SBOM generated for {rel_path.name}"
+ if isinstance(result, sql.Quarantined):
+ success += ". Archive validation in progress."
+ return await session.redirect(
+ get.compose.selected,
+ success=success,
+ project_key=str(project_key),
+ version_key=str(version_key),
+ )
diff --git a/atr/sbom/cli.py b/atr/sbom/cli.py
index a9cbd801..48e66150 100644
--- a/atr/sbom/cli.py
+++ b/atr/sbom/cli.py
@@ -20,6 +20,10 @@ import pathlib
import sys
import yyjson
+from cyclonedx.model.bom import Bom
+from cyclonedx.output import make_outputter
+from cyclonedx.schema import OutputFormat
+from cyclonedx.schema.schema import SCHEMA_VERSIONS
from . import models, osv
from .conformance import ntia_2021_issues
@@ -57,14 +61,21 @@ def command_merge(bundle: models.bundle.Bundle) -> None:
patch_ops = asyncio.run(bundle_to_ntia_patch(bundle))
if patch_ops:
patch_data = patch_to_data(patch_ops)
- merged = bundle.doc.patch(yyjson.Document(patch_data))
- print(merged.dumps())
+ output = bundle.doc.patch(yyjson.Document(patch_data))
+ else:
+ output = bundle.doc
+ if bundle.source_type == "json":
+ print(output.dumps())
else:
- print(bundle.doc.dumps())
+ bom: Bom | None = Bom.from_json(data=output.as_obj)
+ if bom is None:
+ print("Could not generate patched Bom")
+ return
+ print(make_outputter(bom, OutputFormat.XML,
bundle.spec_version).output_as_string(indent=2))
def command_missing(bundle: models.bundle.Bundle) -> None:
- _warnings, errors = ntia_2021_issues(bundle.bom)
+ _warnings, errors = ntia_2021_issues(bundle)
for error in errors:
print(error)
@@ -145,7 +156,7 @@ def command_validate_py(bundle: models.bundle.Bundle) ->
None:
def command_where(bundle: models.bundle.Bundle) -> None:
- _warnings, errors = ntia_2021_issues(bundle.bom)
+ _warnings, errors = ntia_2021_issues(bundle)
for error in errors:
match error:
case models.conformance.MissingProperty():
@@ -154,11 +165,11 @@ def command_where(bundle: models.bundle.Bundle) -> None:
case models.conformance.MissingComponentProperty():
components = bundle.bom.components
primary_component = bundle.bom.metadata.component if
bundle.bom.metadata else None
- if (error.index is not None) and (components is not None):
- print(components[error.index].model_dump_json(indent=2))
+ if (error.index is not None) and len(components) > 0:
+
print(components[error.index].as_json(SCHEMA_VERSIONS[bundle.spec_version]))
print()
elif primary_component is not None:
- print(primary_component.model_dump_json(indent=2))
+
print(primary_component.as_json(SCHEMA_VERSIONS[bundle.spec_version]))
print()
@@ -172,6 +183,8 @@ def main() -> None: # noqa: C901
sys.exit(1)
path = pathlib.Path(sys.argv[2])
bundle = path_to_bundle(path)
+ if not bundle:
+ raise RuntimeError("Could not load bundle")
match sys.argv[1]:
case "license":
command_license(bundle)
diff --git a/atr/sbom/conformance.py b/atr/sbom/conformance.py
index 96b0f638..7d501f1f 100644
--- a/atr/sbom/conformance.py
+++ b/atr/sbom/conformance.py
@@ -19,6 +19,7 @@ from __future__ import annotations
import datetime
import urllib.parse
+from typing import TYPE_CHECKING
import aiohttp
import yyjson
@@ -27,6 +28,11 @@ from . import constants, models
from .maven import cache_read, cache_write
from .utilities import get_pointer
+if TYPE_CHECKING:
+ from cyclonedx.model.component import Component
+
+ from .models.bundle import Bundle
+
def assemble_component_identifier(doc: yyjson.Document, patch_ops:
models.patch.Patch, index: int) -> None:
# May be able to derive this from other fields
@@ -234,7 +240,7 @@ def assemble_metadata_timestamp(doc: yyjson.Document,
patch_ops: models.patch.Pa
def ntia_2021_issues(
- bom_value: models.bom.Bom,
+ bundle: Bundle,
) -> tuple[list[models.conformance.Missing], list[models.conformance.Missing]]:
# 1. Supplier
# ECMA-424 1st edition says that this is the supplier of the primary
component
@@ -270,13 +276,15 @@ def ntia_2021_issues(
warnings: list[models.conformance.Missing] = []
errors: list[models.conformance.Missing] = []
+ bom_value = bundle.bom
+ original_metadata = bundle.doc.get_pointer("/metadata")
- if bom_value.metadata is not None:
+ if bom_value.metadata:
if bom_value.metadata.supplier is None:
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_SUPPLIER))
if bom_value.metadata.component is not None:
- if bom_value.metadata.component.name is None:
+ if not bom_value.metadata.component.name:
errors.append(
models.conformance.MissingComponentProperty(property=models.conformance.ComponentProperty.NAME)
)
@@ -299,19 +307,19 @@ def ntia_2021_issues(
else:
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_COMPONENT))
- if bom_value.metadata.author is None:
+ if len(bom_value.metadata.authors) < 1:
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_AUTHOR))
-
- if bom_value.metadata.timestamp is None:
+ if original_metadata.get("timestamp") is None:
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_TIMESTAMP))
else:
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA))
- for index, component in enumerate(bom_value.components or []):
+ components: list[Component] = list(bom_value.components)
+ for index, component in enumerate(components):
component_type = component.type
component_friendly_name = component.name
- if component_type is not None:
- component_friendly_name = f"{component_type}:
{component_friendly_name}"
+ if component_type:
+ component_friendly_name = f"{component_type.value}:
{component_friendly_name}"
if component.supplier is None:
errors.append(
models.conformance.MissingComponentProperty(
@@ -321,7 +329,7 @@ def ntia_2021_issues(
)
)
- if component.name is None:
+ if not component.name:
errors.append(
models.conformance.MissingComponentProperty(
property=models.conformance.ComponentProperty.NAME,
@@ -330,7 +338,7 @@ def ntia_2021_issues(
)
)
- if component.version is None:
+ if not component.version:
errors.append(
models.conformance.MissingComponentProperty(
property=models.conformance.ComponentProperty.VERSION,
diff --git a/atr/sbom/constants/licenses.py b/atr/sbom/constants/licenses.py
index 299c2521..0e0fcee6 100644
--- a/atr/sbom/constants/licenses.py
+++ b/atr/sbom/constants/licenses.py
@@ -62,6 +62,7 @@ LICENSES: Final[dict[str, list[str]]] = {
"PHP-3.01",
"PostgreSQL",
"Python-2.0",
+ "PSF-2.0",
"SMLNJ",
"TCL",
"UPL-1.0",
diff --git a/atr/sbom/cyclonedx.py b/atr/sbom/cyclonedx.py
index 35459798..0dbfe5d4 100644
--- a/atr/sbom/cyclonedx.py
+++ b/atr/sbom/cyclonedx.py
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
import cyclonedx.exception
import cyclonedx.schema
+import cyclonedx.validation
import cyclonedx.validation.json
from .utilities import get_pointer
@@ -35,19 +36,19 @@ if TYPE_CHECKING:
def validate_cli(bundle_value: models.bundle.Bundle) -> list[str] | None:
args = [
- "cyclonedx",
- "validate",
- "--fail-on-errors",
- "--input-format",
- "json",
- "--input-file",
+ 'cyclonedx',
+ 'validate',
+ '--fail-on-errors',
+ '--input-format',
+ bundle_value.source_type,
+ '--input-file',
bundle_value.path.as_posix(),
]
env = os.environ.copy()
- env["DOTNET_GCHeapHardLimitSOH"] = "0x03000000"
- env["DOTNET_GCHeapHardLimitLOH"] = "0x01000000"
- env["DOTNET_GCHeapHardLimitPOH"] = "0x01000000"
- env["DOTNET_GCHeapCount"] = "1"
+ env['DOTNET_GCHeapHardLimitSOH'] = '0x03000000'
+ env['DOTNET_GCHeapHardLimitLOH'] = '0x01000000'
+ env['DOTNET_GCHeapHardLimitPOH'] = '0x01000000'
+ env['DOTNET_GCHeapCount'] = '1'
proc = subprocess.run(
args,
text=True,
@@ -55,7 +56,7 @@ def validate_cli(bundle_value: models.bundle.Bundle) ->
list[str] | None:
env=env,
)
if proc.returncode != 0:
- err = proc.stdout.strip() or proc.stderr.strip() or "cyclonedx failed"
+ err = proc.stdout.strip() or proc.stderr.strip() or 'cyclonedx failed'
return err.splitlines()
return None
@@ -63,7 +64,7 @@ def validate_cli(bundle_value: models.bundle.Bundle) ->
list[str] | None:
def validate_py(
bundle_value: models.bundle.Bundle,
) -> Iterable[cyclonedx.validation.json.JsonValidationError] | None:
- json_sv = get_pointer(bundle_value.doc, "/specVersion")
+ json_sv = get_pointer(bundle_value.doc, '/specVersion')
schema_version = cyclonedx.schema.SchemaVersion.V1_6
if isinstance(json_sv, str):
schema_version = cyclonedx.schema.SchemaVersion.from_version(json_sv)
diff --git a/atr/sbom/licenses.py b/atr/sbom/licenses.py
index 470571c8..dd8ee692 100644
--- a/atr/sbom/licenses.py
+++ b/atr/sbom/licenses.py
@@ -17,19 +17,27 @@
from __future__ import annotations
+from typing import TYPE_CHECKING
+
+from cyclonedx.model.component import Component
+from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression
+
+if TYPE_CHECKING:
+ from cyclonedx.model.bom import Bom
+
from . import constants, models
from .spdx import license_expression_atoms
def check(
- bom_value: models.bom.Bom,
+ bom_value: Bom,
include_all: bool = False,
) -> tuple[list[models.licenses.Issue], list[models.licenses.Issue],
list[models.licenses.Issue]]:
warnings: list[models.licenses.Issue] = []
errors: list[models.licenses.Issue] = []
good: list[models.licenses.Issue] = []
- components = bom_value.components or []
+ components: list[Component] = list(bom_value.components)
if bom_value.metadata and bom_value.metadata.component:
components = [bom_value.metadata.component, *components]
@@ -45,16 +53,16 @@ def check(
for license_choice in component.licenses:
license_expr = None
- if license_choice.expression:
- license_expr = license_choice.expression
- elif license_choice.license and license_choice.license.id:
- license_expr = license_choice.license.id
+ if isinstance(license_choice, LicenseExpression):
+ license_expr = license_choice.value
+ elif isinstance(license_choice, DisjunctiveLicense):
+ license_expr = license_choice.id
if not license_expr:
continue
parse_failed = False
- if license_choice.expression:
+ if isinstance(license_choice, LicenseExpression):
try:
atoms = license_expression_atoms(license_expr)
except ValueError:
diff --git a/atr/sbom/models/__init__.py b/atr/sbom/models/__init__.py
index 15cf734c..5735eb71 100644
--- a/atr/sbom/models/__init__.py
+++ b/atr/sbom/models/__init__.py
@@ -17,6 +17,6 @@
from __future__ import annotations
-from . import base, bom, bundle, conformance, licenses, osv, patch, sbomqs,
tool
+from . import base, bundle, conformance, licenses, osv, patch, sbomqs, tool
-__all__ = ["base", "bom", "bundle", "conformance", "licenses", "osv", "patch",
"sbomqs", "tool"]
+__all__ = ["base", "bundle", "conformance", "licenses", "osv", "patch",
"sbomqs", "tool"]
diff --git a/atr/sbom/models/bom.py b/atr/sbom/models/bom.py
deleted file mode 100644
index e02dff26..00000000
--- a/atr/sbom/models/bom.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-from __future__ import annotations
-
-import pydantic
-
-from .base import Lax
-
-
-class Swid(Lax):
- tag_id: str | None = pydantic.Field(default=None, alias="tagId")
-
-
-class Supplier(Lax):
- name: str | None = None
- url: list[str] | None = None
-
-
-class License(Lax):
- id: str | None = None
- name: str | None = None
- url: str | None = None
-
-
-class LicenseChoice(Lax):
- license: License | None = None
- expression: str | None = None
-
-
-class Component(Lax):
- bom_ref: str | None = pydantic.Field(default=None, alias="bom-ref")
- name: str | None = None
- version: str | None = None
- supplier: Supplier | None = None
- purl: str | None = None
- cpe: str | None = None
- swid: Swid | None = None
- licenses: list[LicenseChoice] | None = None
- scope: str | None = None
- type: str | None = None
-
-
-class ToolComponent(Lax):
- name: str | None = None
- version: str | None = None
- description: str | None = None
- supplier: Supplier | None = None
-
-
-class ServiceComponent(Lax):
- name: str | None = None
- version: str | None = None
- description: str | None = None
- supplier: Supplier | None = None
- authenticated: bool | None = None
-
-
-class Tool(Lax):
- name: str | None = None
- version: str | None = None
- description: str | None = None
-
-
-class Tools(Lax):
- components: list[ToolComponent] | None = None
- services: list[ServiceComponent] | None = None
-
-
-class Metadata(Lax):
- author: str | None = None
- timestamp: str | None = None
- supplier: Supplier | None = None
- component: Component | None = None
- tools: Tools | list[Tool] | None = None
-
-
-class Dependency(Lax):
- ref: str
- depends_on: list[str] | None = pydantic.Field(default=None,
alias="dependsOn")
-
-
-class Bom(Lax):
- metadata: Metadata | None = None
- components: list[Component] | None = None
- dependencies: list[Dependency] | None = None
diff --git a/atr/sbom/models/bundle.py b/atr/sbom/models/bundle.py
index 800e70e0..5ab3d83f 100644
--- a/atr/sbom/models/bundle.py
+++ b/atr/sbom/models/bundle.py
@@ -18,19 +18,21 @@
from __future__ import annotations
import dataclasses
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Literal
if TYPE_CHECKING:
import pathlib
import yyjson
-
- from .bom import Bom
+ from cyclonedx.model.bom import Bom
+ from cyclonedx.schema import SchemaVersion
@dataclasses.dataclass
class Bundle:
- doc: yyjson.Document
+ source_type: Literal["json", "xml"]
+ spec_version: SchemaVersion
bom: Bom
+ doc: yyjson.Document
path: pathlib.Path
text: str
diff --git a/atr/sbom/osv.py b/atr/sbom/osv.py
index 227c7816..a8f658f9 100644
--- a/atr/sbom/osv.py
+++ b/atr/sbom/osv.py
@@ -20,6 +20,8 @@ from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any
+from packageurl import PackageURL
+
import atr.util as util
from . import models
@@ -28,6 +30,7 @@ from .utilities import get_pointer, osv_severity_to_cdx
if TYPE_CHECKING:
import aiohttp
import yyjson
+ from cyclonedx.model.component import Component
_DEBUG: bool = os.environ.get("DEBUG_SBOM_TOOL") == "1"
_OSV_API_BASE: str = "https://api.osv.dev/v1"
@@ -82,7 +85,7 @@ _SOURCE_DATABASE_NAMES = {
async def scan_bundle(bundle: models.bundle.Bundle) ->
tuple[list[models.osv.ComponentVulnerabilities], list[str]]:
- components = bundle.bom.components or []
+ components = list(bundle.bom.components)
queries, ignored = _scan_bundle_build_queries(components)
if _DEBUG:
print(f"[DEBUG] Scanning {len(queries)} components for
vulnerabilities")
@@ -165,27 +168,24 @@ def _assemble_vulnerabilities(doc: yyjson.Document,
patch_ops: models.patch.Patc
)
-def _component_purl_with_version(component: models.bom.Component) -> str |
None:
+def _component_purl_with_version(component: Component) -> str | None:
+ # If we don't know the purl, we can't help
if component.purl is None:
return None
+ # If the component purl includes version information, return it
+ if component.purl.version is not None:
+ return str(component.purl)
+ # If not, and we don't have a component version, we still can't add
anything
if component.version is None:
return None
version = component.version.strip()
if not version:
return None
- purl = component.purl
- split_index = len(purl)
- question_index = purl.find("?")
- if (question_index != -1) and (question_index < split_index):
- split_index = question_index
- hash_index = purl.find("#")
- if (hash_index != -1) and (hash_index < split_index):
- split_index = hash_index
- if "@" in purl[:split_index]:
- return purl
- base = purl[:split_index]
- suffix = purl[split_index:]
- return f"{base}@{version}{suffix}"
+ # Clone the purl so we don't affect the component definition
+ purl = str(component.purl)
+ new_purl = PackageURL.from_string(purl)
+ new_purl.version = version
+ return str(new_purl)
async def _fetch_vulnerabilities_for_batch(
@@ -256,7 +256,7 @@ async def _paginate_query(
def _scan_bundle_build_queries(
- components: list[models.bom.Component],
+ components: list[Component],
) -> tuple[list[tuple[str, dict[str, Any]]], list[str]]:
queries: list[tuple[str, dict[str, Any]]] = []
ignored = []
@@ -266,8 +266,8 @@ def _scan_bundle_build_queries(
ignored.append(component.name)
continue
query = {"package": {"purl": purl_with_version}}
- if component.bom_ref is not None:
- queries.append((component.bom_ref, query))
+ if component.bom_ref:
+ queries.append((str(component.bom_ref), query))
return queries, ignored
diff --git a/atr/sbom/tool.py b/atr/sbom/tool.py
index 08e98802..4ca1447b 100644
--- a/atr/sbom/tool.py
+++ b/atr/sbom/tool.py
@@ -17,7 +17,6 @@
from __future__ import annotations
-import datetime
from typing import TYPE_CHECKING, Any, Final
import semver
@@ -27,6 +26,8 @@ from . import maven, models, utilities
if TYPE_CHECKING:
from collections.abc import Callable
+ from cyclonedx.model.bom import Bom, BomMetaData
+
_KNOWN_TOOLS: Final[dict[str, models.tool.Tool]] = {
# name in file: ( canonical name, friendly name, version callable )
@@ -59,22 +60,17 @@ def outdated_version_core(
return None
-def plugin_outdated_version(bom_value: models.bom.Bom) ->
list[models.tool.Outdated] | None:
- if bom_value.metadata is None:
+def plugin_outdated_version(bom_value: Bom) -> list[models.tool.Outdated] |
None:
+ if _metadata_is_empty(bom_value.metadata):
return [models.tool.OutdatedMissingMetadata()]
- timestamp = bom_value.metadata.timestamp
- if timestamp is None:
- # This quite often isn't available
- # We could use the file mtime, but that's extremely heuristic
- # return OutdatedMissingTimestamp()
- timestamp =
datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
+ timestamp = bom_value.metadata.timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
tools: list[Any] = []
tools_value = bom_value.metadata.tools
if isinstance(tools_value, list):
tools = tools_value
elif tools_value:
- tools = tools_value.components or []
- services = tools_value.services or []
+ tools = list(tools_value.components)
+ services = list(tools_value.services)
tools.extend(services)
errors = []
for tool in tools:
@@ -102,3 +98,9 @@ def version_parse(version_str: str) -> semver.VersionInfo |
None:
return semver.VersionInfo.parse(version_str.lstrip("v"))
except ValueError:
return None
+
+
+def _metadata_is_empty(metadata: BomMetaData) -> bool:
+ if metadata.component is None and metadata.supplier is None:
+ return True
+ return False
diff --git a/atr/sbom/utilities.py b/atr/sbom/utilities.py
index 5d4e94a7..0227a2b5 100644
--- a/atr/sbom/utilities.py
+++ b/atr/sbom/utilities.py
@@ -17,14 +17,21 @@
from __future__ import annotations
+import json
import re
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Literal
import cvss
+from cyclonedx.model.bom import Bom
+from cyclonedx.output import BaseOutput, make_outputter
+from cyclonedx.schema import OutputFormat, SchemaVersion
+from defusedxml import ElementTree
if TYPE_CHECKING:
import pathlib
+ from cyclonedx.model import Property
+
import atr.sbom.models.osv as osv
import yyjson
@@ -74,7 +81,7 @@ def apply_patch(
async def bundle_to_ntia_patch(bundle_value: models.bundle.Bundle) ->
models.patch.Patch:
from .conformance import ntia_2021_issues, ntia_2021_patch
- _warnings, errors = ntia_2021_issues(bundle_value.bom)
+ _warnings, errors = ntia_2021_issues(bundle_value)
async with util.create_secure_session() as session:
patch_ops = await ntia_2021_patch(session, bundle_value.doc, errors)
return patch_ops
@@ -89,6 +96,12 @@ async def bundle_to_vuln_patch(
return patch_ops
+def bundle_outputter(bundle_value: models.bundle.Bundle) -> BaseOutput:
+ return make_outputter(
+ bom=bundle_value.bom, output_format=OutputFormat.JSON,
schema_version=bundle_value.spec_version
+ )
+
+
def cdx_severity_to_osv(severity: list[dict[str, str | float]]) -> tuple[str |
None, list[dict[str, str]]]:
severities = [
{
@@ -111,14 +124,10 @@ def get_pointer(doc: yyjson.Document, path: str) -> Any |
None:
raise
-def get_props_from_bundle(bundle_value: models.bundle.Bundle) -> tuple[int,
list[dict[str, str]]]:
- version: int | None = get_pointer(bundle_value.doc, "/version")
- if version is None:
- version = 0
- properties: list[dict[str, str]] | None = get_pointer(bundle_value.doc,
"/properties")
- if properties is None:
- return version, []
- return version, [p for p in properties if "asf:atr:" in p.get("name", "")]
+def get_props_from_bundle(bundle_value: models.bundle.Bundle) -> tuple[int,
list[Property]]:
+ version: int = bundle_value.bom.version
+ properties = bundle_value.bom.properties
+ return version, [p for p in properties if "asf:atr:" in p.name]
def osv_severity_to_cdx(severity: list[dict[str, Any]] | None, textual: str)
-> list[dict[str, str | float]] | None:
@@ -140,8 +149,35 @@ def patch_to_data(patch_ops: models.patch.Patch) ->
list[dict[str, Any]]:
def path_to_bundle(path: pathlib.Path) -> models.bundle.Bundle:
text = path.read_text(encoding="utf-8")
- bom = models.bom.Bom.model_validate_json(text)
- return models.bundle.Bundle(doc=yyjson.Document(text), bom=bom, path=path,
text=text)
+ bom: Bom | None = None
+ source_type: Literal["json", "xml"] | None = None
+ # Default to latest schema version
+ spec_version: SchemaVersion | None = None
+ if path.name.endswith(".json"):
+ bom_json: dict[str, Any] = json.loads(text)
+ bom = Bom.from_json(data=bom_json)
+ source_type = "json"
+ version_str = bom_json.get("specVersion", "1.7")
+ spec_version = SchemaVersion.from_version(version_str)
+ elif path.name.endswith(".xml"):
+ bom_xml = ElementTree.fromstring(text)
+ bom = Bom.from_xml(bom_xml)
+ tag = re.match(r"\{http://cyclonedx.org/schema/bom/(.+)}", bom_xml.tag)
+ version = "1.7"
+ if tag:
+ version = tag.group(1)
+ spec_version = SchemaVersion.from_version(version)
+ source_type = "xml"
+ if bom:
+ outputter: BaseOutput = make_outputter(
+ bom=bom, output_format=OutputFormat.JSON,
schema_version=spec_version
+ )
+ text: str = outputter.output_as_string()
+ if not bom or not source_type or not spec_version:
+ raise ValueError("Error importing BOM")
+ return models.bundle.Bundle(
+ doc=yyjson.Document(text), bom=bom, path=path, text=text,
source_type=source_type, spec_version=spec_version
+ )
def _extract_cdx_score(type: str, score_str: str) -> dict[str, str | float]:
diff --git a/atr/storage/writers/sbom.py b/atr/storage/writers/sbom.py
index 326a0d0b..f204d824 100644
--- a/atr/storage/writers/sbom.py
+++ b/atr/storage/writers/sbom.py
@@ -105,6 +105,33 @@ class CommitteeParticipant(FoundationCommitter):
await self.__data.refresh(sbom_task)
return sbom_task
+ async def convert_cyclonedx(
+ self,
+ project_key: safe.ProjectKey,
+ version_key: safe.VersionKey,
+ revision_number: safe.RevisionNumber,
+ file_path: str,
+ sbom_path: str,
+ ) -> sql.Task:
+ sbom_task = sql.Task(
+ task_type=sql.TaskType.SBOM_CONVERT,
+ task_args=sbom.ConvertCycloneDX(
+ artifact_path=file_path,
+ revision=revision_number,
+ output_path=sbom_path,
+ ).model_dump(),
+ asf_uid=util.unwrap(self.__asf_uid),
+ added=datetime.datetime.now(datetime.UTC),
+ status=sql.TaskStatus.QUEUED,
+ project_key=str(project_key),
+ version_key=str(version_key),
+ revision_number=str(revision_number),
+ )
+ self.__data.add(sbom_task)
+ await self.__data.commit()
+ await self.__data.refresh(sbom_task)
+ return sbom_task
+
async def generate_cyclonedx(
self,
project_key: safe.ProjectKey,
diff --git a/atr/tasks/__init__.py b/atr/tasks/__init__.py
index 7f3879b4..a2cc27a2 100644
--- a/atr/tasks/__init__.py
+++ b/atr/tasks/__init__.py
@@ -323,6 +323,8 @@ def resolve(task_type: sql.TaskType) -> Callable[...,
Awaitable[results.Results
return rat.check
case sql.TaskType.SBOM_AUGMENT:
return sbom.augment
+ case sql.TaskType.SBOM_CONVERT:
+ return sbom.convert_cyclonedx
case sql.TaskType.SBOM_GENERATE_CYCLONEDX:
return sbom.generate_cyclonedx
case sql.TaskType.SBOM_OSV_SCAN:
diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py
index c96e6a6e..15f2ecf9 100644
--- a/atr/tasks/sbom.py
+++ b/atr/tasks/sbom.py
@@ -40,11 +40,27 @@ import atr.util as util
_CONFIG: Final = config.get()
+class ConvertCycloneDX(schema.Strict):
+ """Arguments for the task to generate a CycloneDX SBOM."""
+
+ artifact_path: str = schema.description('Absolute path to the artifact')
+ output_path: str = schema.description('Absolute path where the generated
SBOM JSON should be written')
+ revision: safe.RevisionNumber = schema.description('Revision number')
+
+
class GenerateCycloneDX(schema.Strict):
"""Arguments for the task to generate a CycloneDX SBOM."""
- artifact_path: str = schema.description("Absolute path to the artifact")
- output_path: str = schema.description("Absolute path where the generated
SBOM JSON should be written")
+ artifact_path: str = schema.description('Absolute path to the artifact')
+ output_path: str = schema.description('Absolute path where the generated
SBOM JSON should be written')
+
+
+class SBOMConversionError(Exception):
+ """Custom exception for SBOM conversion failures."""
+
+ def __init__(self, message: str, details: dict[str, Any] | None = None) ->
None:
+ super().__init__(message)
+ self.details = details or {}
class SBOMGenerationError(Exception):
@@ -70,40 +86,40 @@ class SBOMScoringError(Exception):
class FileArgs(schema.Strict):
- project_key: safe.ProjectKey = schema.description("Project name")
- version_key: safe.VersionKey = schema.description("Version name")
- revision_number: safe.RevisionNumber = schema.description("Revision
number")
- file_path: safe.RelPath = schema.description("Relative path to the SBOM
file")
+ project_key: safe.ProjectKey = schema.description('Project name')
+ version_key: safe.VersionKey = schema.description('Version name')
+ revision_number: safe.RevisionNumber = schema.description('Revision
number')
+ file_path: safe.RelPath = schema.description('Relative path to the SBOM
file')
asf_uid: str | None = None
class ScoreArgs(FileArgs):
- previous_release_version: safe.VersionKey | None =
schema.description("Previous release version")
+ previous_release_version: safe.VersionKey | None =
schema.description('Previous release version')
@checks.with_model(FileArgs)
async def augment(args: FileArgs) -> results.Results | None:
- project_str = str(args.project_key)
- version_str = str(args.version_key)
revision_str = str(args.revision_number)
path_str = str(args.file_path)
- base_dir = paths.get_unfinished_dir() / project_str / version_str /
revision_str
+ base_dir = paths.get_unfinished_dir_for(args.project_key,
args.version_key, args.revision_number)
if not await aiofiles.os.path.isdir(base_dir):
- raise SBOMScoringError("Revision directory does not exist",
{"base_dir": str(base_dir)})
+ raise SBOMScoringError('Revision directory does not exist',
{'base_dir': str(base_dir)})
full_path = base_dir / path_str
full_path_str = str(full_path)
- if not (full_path_str.endswith(".cdx.json") and await
aiofiles.os.path.isfile(full_path)):
- raise SBOMScoringError("SBOM file does not exist", {"file_path":
path_str})
+ if not (full_path_str.endswith('.cdx.json') and await
aiofiles.os.path.isfile(full_path)):
+ raise SBOMScoringError('SBOM file does not exist', {'file_path':
path_str})
# Read from the old revision
bundle = sbom.utilities.path_to_bundle(full_path)
+ if not bundle:
+ raise SBOMScoringError('Could not load bundle')
patch_ops = await sbom.utilities.bundle_to_ntia_patch(bundle)
new_full_path: pathlib.Path | None = None
new_full_path_str: str | None = None
new_version = None
if patch_ops:
- new_version, merged = sbom.utilities.apply_patch("augment",
revision_str, bundle, patch_ops)
- description = "SBOM augmentation through web interface"
+ new_version, merged = sbom.utilities.apply_patch('augment',
revision_str, bundle, patch_ops)
+ description = 'SBOM augmentation through web interface'
async with storage.write(args.asf_uid) as write:
wacp = await
write.as_project_committee_participant(args.project_key)
@@ -112,37 +128,52 @@ async def augment(args: FileArgs) -> results.Results |
None:
new_full_path = path / path_str
new_full_path_str = str(new_full_path)
# Write to the new revision
- log.info(f"Writing augmented SBOM to {new_full_path_str}")
+ log.info(f'Writing augmented SBOM to {new_full_path_str}')
await aiofiles.os.remove(new_full_path)
- async with aiofiles.open(new_full_path, "w", encoding="utf-8")
as f:
+ async with aiofiles.open(new_full_path, 'w', encoding='utf-8')
as f:
await f.write(merged.dumps())
await wacp.revision.create_revision_with_quarantine(
- args.project_key, args.version_key, args.asf_uid or "unknown",
description=description, modify=modify
+ args.project_key, args.version_key, args.asf_uid or 'unknown',
description=description, modify=modify
)
return results.SBOMAugment(
- kind="sbom_augment",
+ kind='sbom_augment',
path=(new_full_path_str if (new_full_path_str is not None) else
full_path_str),
bom_version=new_version,
)
[email protected]_model(ConvertCycloneDX)
+async def convert_cyclonedx(args: ConvertCycloneDX) -> results.Results | None:
+ """Generate a JSON CycloneDX SBOM from a given XML SBOM."""
+ try:
+ result_data = await _convert_cyclonedx_core(args.artifact_path,
args.output_path, str(args.revision))
+ log.info(f'Successfully converted CycloneDX SBOM for
{args.artifact_path}')
+ msg = result_data['message']
+ if not isinstance(msg, str):
+ raise SBOMConversionError(f'Invalid message type: {type(msg)}')
+ return results.SBOMConvert(kind='sbom_convert', path=args.output_path,
bom_version=result_data.get('version'))
+ except (archives.ExtractionError, SBOMGenerationError) as e:
+ log.error(f'SBOM conversion failed for {args.artifact_path}: {e}')
+ raise
+
+
@checks.with_model(GenerateCycloneDX)
async def generate_cyclonedx(args: GenerateCycloneDX) -> results.Results |
None:
"""Generate a CycloneDX SBOM for the given artifact and write it to the
output path."""
try:
result_data = await _generate_cyclonedx_core(args.artifact_path,
args.output_path)
- log.info(f"Successfully generated CycloneDX SBOM for
{args.artifact_path}")
- msg = result_data["message"]
+ log.info(f'Successfully generated CycloneDX SBOM for
{args.artifact_path}')
+ msg = result_data['message']
if not isinstance(msg, str):
- raise SBOMGenerationError(f"Invalid message type: {type(msg)}")
+ raise SBOMGenerationError(f'Invalid message type: {type(msg)}')
return results.SBOMGenerateCycloneDX(
- kind="sbom_generate_cyclonedx",
+ kind='sbom_generate_cyclonedx',
msg=msg,
)
except (archives.ExtractionError, SBOMGenerationError) as e:
- log.error(f"SBOM generation failed for {args.artifact_path}: {e}")
+ log.error(f'SBOM generation failed for {args.artifact_path}: {e}')
raise
@@ -153,12 +184,14 @@ async def osv_scan(args: FileArgs) -> results.Results |
None:
base_dir = paths.get_unfinished_dir_for(args.project_key,
args.version_key, args.revision_number)
if not await aiofiles.os.path.isdir(base_dir):
- raise SBOMScanningError("Revision directory does not exist",
{"base_dir": str(base_dir)})
+ raise SBOMScanningError('Revision directory does not exist',
{'base_dir': str(base_dir)})
full_path = base_dir / path_str
full_path_str = str(full_path)
- if not (full_path_str.endswith(".cdx.json") and await
aiofiles.os.path.isfile(full_path)):
- raise SBOMScanningError("SBOM file does not exist", {"file_path":
path_str})
+ if not (full_path_str.endswith('.cdx.json') and await
aiofiles.os.path.isfile(full_path)):
+ raise SBOMScanningError('SBOM file does not exist', {'file_path':
path_str})
bundle = sbom.utilities.path_to_bundle(full_path)
+ if not bundle:
+ raise SBOMScanningError('Could not load bundle')
vulnerabilities, ignored = await sbom.osv.scan_bundle(bundle)
patch_ops = await sbom.utilities.bundle_to_vuln_patch(bundle,
vulnerabilities)
components = []
@@ -174,8 +207,8 @@ async def osv_scan(args: FileArgs) -> results.Results |
None:
new_full_path: pathlib.Path | None = None
new_full_path_str: str | None = None
- new_version, merged = sbom.utilities.apply_patch("osv-scan", revision_str,
bundle, patch_ops)
- description = "SBOM vulnerability scan through web interface"
+ new_version, merged = sbom.utilities.apply_patch('osv-scan', revision_str,
bundle, patch_ops)
+ description = 'SBOM vulnerability scan through web interface'
async with storage.write(args.asf_uid) as write:
wacp = await write.as_project_committee_participant(args.project_key)
@@ -184,17 +217,17 @@ async def osv_scan(args: FileArgs) -> results.Results |
None:
new_full_path = path / str(args.file_path)
new_full_path_str = str(new_full_path)
# Write to the new revision
- log.info(f"Writing updated SBOM to {new_full_path_str}")
+ log.info(f'Writing updated SBOM to {new_full_path_str}')
await aiofiles.os.remove(new_full_path)
- async with aiofiles.open(new_full_path, "w", encoding="utf-8") as
f:
+ async with aiofiles.open(new_full_path, 'w', encoding='utf-8') as
f:
await f.write(merged.dumps())
await wacp.revision.create_revision_with_quarantine(
- args.project_key, args.version_key, args.asf_uid or "unknown",
description=description, modify=modify
+ args.project_key, args.version_key, args.asf_uid or 'unknown',
description=description, modify=modify
)
return results.SBOMOSVScan(
- kind="sbom_osv_scan",
+ kind='sbom_osv_scan',
project_key=args.project_key,
version_key=args.version_key,
revision_number=args.revision_number,
@@ -212,16 +245,16 @@ async def score_qs(args: FileArgs) -> results.Results |
None:
base_dir = paths.get_unfinished_dir_for(args.project_key,
args.version_key, args.revision_number)
if not await aiofiles.os.path.isdir(base_dir):
- raise SBOMScoringError("Revision directory does not exist",
{"base_dir": str(base_dir)})
+ raise SBOMScoringError('Revision directory does not exist',
{'base_dir': str(base_dir)})
full_path = base_dir / path_str
full_path_str = str(full_path)
- if not (full_path_str.endswith(".cdx.json") and await
aiofiles.os.path.isfile(full_path)):
- raise SBOMScoringError("SBOM file does not exist", {"file_path":
path_str})
+ if not (full_path_str.endswith('.cdx.json') and await
aiofiles.os.path.isfile(full_path)):
+ raise SBOMScoringError('SBOM file does not exist', {'file_path':
path_str})
proc = await asyncio.create_subprocess_exec(
- "sbomqs",
- "score",
+ 'sbomqs',
+ 'score',
full_path.name,
- "--json",
+ '--json',
cwd=str(full_path.parent),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
@@ -230,12 +263,12 @@ async def score_qs(args: FileArgs) -> results.Results |
None:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
if proc.returncode != 0:
raise SBOMScoringError(
- "sbomqs command failed",
- {"returncode": proc.returncode, "stderr": stderr.decode("utf-8",
"ignore")},
+ 'sbomqs command failed',
+ {'returncode': proc.returncode, 'stderr': stderr.decode('utf-8',
'ignore')},
)
- report_obj =
results.SbomQsReport.model_validate(json.loads(stdout.decode("utf-8")))
+ report_obj =
results.SbomQsReport.model_validate(json.loads(stdout.decode('utf-8')))
return results.SBOMQsScore(
- kind="sbom_qs_score",
+ kind='sbom_qs_score',
project_key=args.project_key,
version_key=args.version_key,
revision_number=args.revision_number,
@@ -253,14 +286,16 @@ async def score_tool(args: ScoreArgs) -> results.Results
| None:
if args.previous_release_version is not None:
previous_base_dir = paths.get_finished_dir_for(args.project_key,
args.previous_release_version)
if not await aiofiles.os.path.isdir(base_dir):
- raise SBOMScoringError("Revision directory does not exist",
{"base_dir": str(base_dir)})
+ raise SBOMScoringError('Revision directory does not exist',
{'base_dir': str(base_dir)})
full_path = base_dir / path_str
full_path_str = str(full_path)
- if not (full_path_str.endswith(".cdx.json") and await
aiofiles.os.path.isfile(full_path)):
- raise SBOMScoringError("SBOM file does not exist", {"file_path":
path_str})
+ if not (full_path_str.endswith('.cdx.json') and await
aiofiles.os.path.isfile(full_path)):
+ raise SBOMScoringError('SBOM file does not exist', {'file_path':
path_str})
bundle = sbom.utilities.path_to_bundle(full_path)
+ if not bundle:
+ raise SBOMScoringError('Could not load bundle')
version, properties = sbom.utilities.get_props_from_bundle(bundle)
- warnings, errors = sbom.conformance.ntia_2021_issues(bundle.bom)
+ warnings, errors = sbom.conformance.ntia_2021_issues(bundle)
# TODO: Could update the ATR version with a constant showing last change
to the augment/scan
# tools so we know if it's outdated
outdated = sbom.tool.plugin_outdated_version(bundle.bom)
@@ -287,7 +322,7 @@ async def score_tool(args: ScoreArgs) -> results.Results |
None:
prev_vulnerabilities = sbom.osv.vulns_from_bundle(previous_bundle)
return results.SBOMToolScore(
- kind="sbom_tool_score",
+ kind='sbom_tool_score',
project_key=args.project_key,
version_key=args.version_key,
revision_number=args.revision_number,
@@ -302,7 +337,7 @@ async def score_tool(args: ScoreArgs) -> results.Results |
None:
vulnerabilities=[v.model_dump_json() for v in vulnerabilities],
prev_licenses=[w.model_dump_json() for w in prev_licenses] if
prev_licenses else None,
prev_vulnerabilities=[v.model_dump_json() for v in
prev_vulnerabilities] if prev_vulnerabilities else None,
- atr_props=properties,
+ atr_props=[{p.name: p.value or ''} for p in properties],
cli_errors=cli_errors,
)
@@ -310,9 +345,9 @@ async def score_tool(args: ScoreArgs) -> results.Results |
None:
def _extracted_dir(temp_dir: str) -> str | None:
# Loop through all the dirs in temp_dir
extract_dir = None
- log.info(f"Checking directories in {temp_dir}: {os.listdir(temp_dir)}")
+ log.info(f'Checking directories in {temp_dir}: {os.listdir(temp_dir)}')
for dir_name in os.listdir(temp_dir):
- if dir_name.startswith("."):
+ if dir_name.startswith('.'):
continue
dir_path = os.path.join(temp_dir, dir_name)
if os.path.isdir(dir_path):
@@ -325,13 +360,41 @@ def _extracted_dir(temp_dir: str) -> str | None:
return extract_dir
+async def _convert_cyclonedx_core(artifact_path: str, output_path: str,
revision_str: str) -> dict[str, Any]:
+ """Core logic to convert XML CycloneDX SBOM to JSON."""
+ log.info(f'Generating CycloneDX JSON SBOM for {artifact_path} ->
{output_path}')
+
+ # TODO: Should create a new revision here rather than in the caller
+ bundle = sbom.utilities.path_to_bundle(pathlib.Path(artifact_path))
+ if not bundle:
+ raise SBOMConversionError('Could not load bundle')
+ sbom.utilities.apply_patch('conversion to JSON', revision_str, bundle, [])
+ outputter = sbom.utilities.bundle_outputter(bundle)
+ text = outputter.output_as_string(indent=2)
+
+ try:
+ async with aiofiles.open(output_path, 'w', encoding='utf-8') as f:
+ await f.write(text)
+ log.info(f'Successfully wrote JSON SBOM to {output_path}')
+ except Exception as write_err:
+ log.exception(f'Failed to write SBOM JSON to {output_path}:
{write_err}')
+ raise SBOMConversionError(f'Failed to write SBOM to {output_path}:
{write_err}') from write_err
+
+ return {
+ 'message': 'Successfully generated and saved CycloneDX SBOM',
+ 'sbom': text,
+ 'format': 'CycloneDX',
+ 'version': str(bundle.bom.version),
+ }
+
+
async def _generate_cyclonedx_core(artifact_path: str, output_path: str) ->
dict[str, Any]:
"""Core logic to generate CycloneDX SBOM on failure."""
- log.info(f"Generating CycloneDX SBOM for {artifact_path} -> {output_path}")
+ log.info(f'Generating CycloneDX SBOM for {artifact_path} -> {output_path}')
# TODO: Should create a new revision here rather than in the caller
- async with util.async_temporary_directory(prefix="cyclonedx_sbom_") as
temp_dir:
- log.info(f"Created temporary directory: {temp_dir}")
+ async with util.async_temporary_directory(prefix='cyclonedx_sbom_') as
temp_dir:
+ log.info(f'Created temporary directory: {temp_dir}')
# # Find and validate the root directory
# try:
@@ -347,7 +410,7 @@ async def _generate_cyclonedx_core(artifact_path: str,
output_path: str) -> dict
# Extract the archive to the temporary directory
# TODO: Ideally we'd have task dependencies or archive caching
- log.info(f"Extracting {artifact_path} to {temp_dir}")
+ log.info(f'Extracting {artifact_path} to {temp_dir}')
extracted_size, _extracted_paths = await asyncio.to_thread(
archives.extract,
artifact_path,
@@ -355,22 +418,22 @@ async def _generate_cyclonedx_core(artifact_path: str,
output_path: str) -> dict
max_size=_CONFIG.MAX_EXTRACT_SIZE,
chunk_size=_CONFIG.EXTRACT_CHUNK_SIZE,
)
- log.info(f"Extracted {extracted_size} bytes")
+ log.info(f'Extracted {extracted_size} bytes')
# Find the root directory
if (extract_dir := _extracted_dir(str(temp_dir))) is None:
- log.error("No root directory found in archive")
+ log.error('No root directory found in archive')
return {
- "valid": False,
- "message": "No root directory found in archive",
- "errors": [],
+ 'valid': False,
+ 'message': 'No root directory found in archive',
+ 'errors': [],
}
- log.info(f"Using root directory: {extract_dir}")
+ log.info(f'Using root directory: {extract_dir}')
# Run syft to generate the CycloneDX SBOM
- syft_command = ["syft", extract_dir, "-o", "cyclonedx-json",
"--enrich", "all", "--base-path", f"{temp_dir!s}"]
- log.info(f"Running syft: {' '.join(syft_command)}")
+ syft_command = ['syft', extract_dir, '-o', 'cyclonedx-json',
'--enrich', 'all', '--base-path', f'{temp_dir!s}']
+ log.info(f'Running syft: {" ".join(syft_command)}')
try:
process = await asyncio.create_subprocess_exec(
@@ -380,48 +443,48 @@ async def _generate_cyclonedx_core(artifact_path: str,
output_path: str) -> dict
)
stdout, stderr = await asyncio.wait_for(process.communicate(),
timeout=300)
- stdout_str = stdout.decode("utf-8").strip() if stdout else ""
- stderr_str = stderr.decode("utf-8").strip() if stderr else ""
+ stdout_str = stdout.decode('utf-8').strip() if stdout else ''
+ stderr_str = stderr.decode('utf-8').strip() if stderr else ''
if process.returncode != 0:
- log.error(f"syft command failed with code
{process.returncode}")
- log.error(f"syft stderr: {stderr_str}")
- log.error(f"syft stdout: {stdout_str[:1000]}...")
+ log.error(f'syft command failed with code
{process.returncode}')
+ log.error(f'syft stderr: {stderr_str}')
+ log.error(f'syft stdout: {stdout_str[:1000]}...')
raise SBOMGenerationError(
- f"syft command failed with code {process.returncode}",
- {"returncode": process.returncode, "stderr": stderr_str,
"stdout": stdout_str[:1000]},
+ f'syft command failed with code {process.returncode}',
+ {'returncode': process.returncode, 'stderr': stderr_str,
'stdout': stdout_str[:1000]},
)
# Parse the JSON output from syft
try:
sbom_data = json.loads(stdout_str)
- log.info(f"Successfully parsed syft output for
{artifact_path}")
+ log.info(f'Successfully parsed syft output for
{artifact_path}')
# Write the SBOM data to the specified output path
try:
- async with aiofiles.open(output_path, "w",
encoding="utf-8") as f:
+ async with aiofiles.open(output_path, 'w',
encoding='utf-8') as f:
await f.write(json.dumps(sbom_data, indent=2))
- log.info(f"Successfully wrote SBOM to {output_path}")
+ log.info(f'Successfully wrote SBOM to {output_path}')
except Exception as write_err:
- log.exception(f"Failed to write SBOM JSON to
{output_path}: {write_err}")
- raise SBOMGenerationError(f"Failed to write SBOM to
{output_path}: {write_err}") from write_err
+ log.exception(f'Failed to write SBOM JSON to
{output_path}: {write_err}')
+ raise SBOMGenerationError(f'Failed to write SBOM to
{output_path}: {write_err}') from write_err
return {
- "message": "Successfully generated and saved CycloneDX
SBOM",
- "sbom": sbom_data,
- "format": "CycloneDX",
- "components": len(sbom_data.get("components", [])),
+ 'message': 'Successfully generated and saved CycloneDX
SBOM',
+ 'sbom': sbom_data,
+ 'format': 'CycloneDX',
+ 'components': len(sbom_data.get('components', [])),
}
except json.JSONDecodeError as e:
- log.error(f"Failed to parse syft output as JSON: {e}")
+ log.error(f'Failed to parse syft output as JSON: {e}')
raise SBOMGenerationError(
- f"Failed to parse syft output: {e}",
- {"error": str(e), "syft_output": stdout_str[:1000]},
+ f'Failed to parse syft output: {e}',
+ {'error': str(e), 'syft_output': stdout_str[:1000]},
) from e
except TimeoutError:
- log.error("syft command timed out after 5 minutes")
- raise SBOMGenerationError("syft command timed out after 5 minutes")
+ log.error('syft command timed out after 5 minutes')
+ raise SBOMGenerationError('syft command timed out after 5 minutes')
except FileNotFoundError:
- log.error("syft command not found. Is it installed and in PATH?")
- raise SBOMGenerationError("syft command not found")
+ log.error('syft command not found. Is it installed and in PATH?')
+ raise SBOMGenerationError('syft command not found')
diff --git a/atr/templates/draft-tools.html b/atr/templates/draft-tools.html
index 38851178..0dc4ef47 100644
--- a/atr/templates/draft-tools.html
+++ b/atr/templates/draft-tools.html
@@ -36,4 +36,12 @@
<p>Generate a CycloneDX Software Bill of Materials (SBOM) file for this
artifact.</p>
{{ sbom_form|safe }}
{% endif %}
+ {% if file_path.endswith(".cdx.xml") and
is_viewing_as_admin_fn(current_user.uid) %}
+ <h3>Convert SBOM</h3>
+ <div class="alert-info">
+ <p>NOTE: This functionality is in early release.</p>
+ </div>
+ <p>Convert this XML CycloneDX Software Bill of Materials (SBOM) into a
JSON equivalent.</p>
+ {{ sbom_convert_form|safe }}
+ {% endif %}
{% endblock content %}
diff --git a/pip-audit.requirements b/pip-audit.requirements
index c70194ab..0d628f41 100644
--- a/pip-audit.requirements
+++ b/pip-audit.requirements
@@ -5,7 +5,7 @@ aiofiles==25.1.0
# tooling-trusted-releases
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.13.3
+aiohttp==3.13.4
# via
# asfpy
# asfquart
@@ -30,7 +30,7 @@ arrow==1.4.0
# via isoduration
asfpy==0.58
# via asfquart
-asfquart @
git+https://github.com/apache/infrastructure-asfquart.git@788bbbcca5552644ed669cd0bbd4bee295da8f9a
+asfquart @
git+https://github.com/apache/infrastructure-asfquart.git@fab20db357dd689ae5863c86d9eb1b07ae690b83
# via tooling-trusted-releases
asyncssh==2.22.0
# via tooling-trusted-releases
@@ -72,7 +72,7 @@ colorama==0.4.6
# click
# djlint
# tqdm
-cryptography==46.0.5
+cryptography==46.0.6
# via
# asfpy
# asyncssh
@@ -108,7 +108,7 @@ editorconfig==0.17.1
# jsbeautifier
email-validator==2.3.0
# via tooling-trusted-releases
-exarch==0.2.8
+exarch==0.2.9
# via tooling-trusted-releases
ezt==1.1
# via
@@ -175,7 +175,7 @@ jsbeautifier==1.15.4
# via
# cssbeautifier
# djlint
-json5==0.13.0
+json5==0.14.0
# via djlint
jsonpointer==3.1.1
# via jsonschema
@@ -258,7 +258,7 @@ pydantic-core==2.41.5
# pydantic-xml
pydantic-xml==2.19.0
# via tooling-trusted-releases
-pygments==2.19.2
+pygments==2.20.0
# via rich
pyhumps==3.8.0
# via quart-schema
@@ -271,7 +271,7 @@ python-dateutil==2.9.0.post0
# strictyaml
python-decouple==3.8
# via tooling-trusted-releases
-python-discovery==1.2.0
+python-discovery==1.2.1
# via virtualenv
python-gnupg==0.5.6
# via tooling-trusted-releases
@@ -301,7 +301,7 @@ referencing==0.37.0
# cyclonedx-python-lib
# jsonschema
# jsonschema-specifications
-regex==2026.2.28
+regex==2026.3.32
# via djlint
requests==2.33.0
# via asfpy
@@ -317,7 +317,7 @@ rpds-py==0.30.0
# via
# jsonschema
# referencing
-ruff==0.15.7
+ruff==0.15.8
semver==3.0.4
# via tooling-trusted-releases
six==1.17.0
diff --git a/pyproject.toml b/pyproject.toml
index 08893f90..973d0c1c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -164,3 +164,6 @@ select = [
"atr/sbom/conformance.py" = ["C901"]
"atr/sbom/licenses.py" = ["C901"]
"migrations/env.py" = ["E402"]
+
+[tool.ruff.format]
+quote-style = "single"
diff --git a/typestubs/cyclonedx/model/bom.pyi
b/typestubs/cyclonedx/model/bom.pyi
new file mode 100644
index 00000000..d543f8c0
--- /dev/null
+++ b/typestubs/cyclonedx/model/bom.pyi
@@ -0,0 +1,44 @@
+from collections.abc import Sequence
+from datetime import datetime
+from typing import Any, Optional, Type, Dict
+
+from cyclonedx.model.component import Component
+from cyclonedx.model.contact import OrganizationalEntity
+from py_serializable import _JsonSerializable, _XmlSerializable, _T
+
+
+class BomMetaData:
+ component: Optional[Component]
+ supplier: Optional[OrganizationalEntity]
+ authors: Any
+ timestamp: datetime
+ tools: Any
+
+
+class Bom(_JsonSerializable, _XmlSerializable):
+ def __init__(self, **kwargs: Any) -> None: ...
+
+ @property
+ def version(self) -> int: ...
+ @version.setter
+ def version(self, version: int) -> None: ...
+
+ @property
+ def metadata(self) -> BomMetaData: ...
+ @metadata.setter
+ def metadata(self, metadata: BomMetaData) -> None: ...
+
+ @property
+ def components(self) -> Sequence[Component]: ...
+ @components.setter
+ def components(self, components: Any) -> None: ...
+
+ @property
+ def properties(self) -> Any: ...
+ @properties.setter
+ def properties(self, properties: Any) -> None: ...
+
+ @property
+ def dependencies(self) -> Any: ...
+ @dependencies.setter
+ def dependencies(self, dependencies: Any) -> None: ...
diff --git a/typestubs/cyclonedx/model/component.pyi
b/typestubs/cyclonedx/model/component.pyi
new file mode 100644
index 00000000..6ff78ec9
--- /dev/null
+++ b/typestubs/cyclonedx/model/component.pyi
@@ -0,0 +1,48 @@
+from enum import Enum
+from typing import Any, Optional
+
+from cyclonedx.model.bom_ref import BomRef
+from cyclonedx.model.contact import OrganizationalEntity
+from cyclonedx.model.license import LicenseRepository
+from packageurl import PackageURL
+from py_serializable import _JsonSerializable, _XmlSerializable
+
+
+class ComponentScope(str, Enum):
+ REQUIRED = "required"
+ OPTIONAL = "optional"
+ EXCLUDED = "excluded"
+
+
+class ComponentType(str, Enum):
+ APPLICATION = "application"
+ CONTAINER = "container"
+ CRYPTOGRAPHIC_ASSET = "cryptographic-asset"
+ DATA = "data"
+ DEVICE = "device"
+ DEVICE_DRIVER = "device-driver"
+ FILE = "file"
+ FIRMWARE = "firmware"
+ FRAMEWORK = "framework"
+ LIBRARY = "library"
+ MACHINE_LEARNING_MODEL = "machine-learning-model"
+ OPERATING_SYSTEM = "operating-system"
+ PLATFORM = "platform"
+
+
+class Swid: ...
+
+
+class Component(_JsonSerializable, _XmlSerializable):
+ name: str
+ version: Optional[str]
+ type: ComponentType
+ supplier: Optional[OrganizationalEntity]
+ cpe: Optional[str]
+ purl: Optional[PackageURL]
+ swid: Optional[Swid]
+ scope: Optional[ComponentScope]
+ licenses: LicenseRepository
+ bom_ref: BomRef
+
+ def __init__(self, *, component_type: ComponentType, name: str, **kwargs:
Any) -> None: ...
diff --git a/typestubs/cyclonedx/output/__init__.pyi
b/typestubs/cyclonedx/output/__init__.pyi
new file mode 100644
index 00000000..49c0631a
--- /dev/null
+++ b/typestubs/cyclonedx/output/__init__.pyi
@@ -0,0 +1,22 @@
+from abc import abstractmethod
+from typing import Any, Optional, Union
+
+from cyclonedx.model.bom import Bom
+from cyclonedx.schema import OutputFormat, SchemaVersion
+
+
+class BaseOutput:
+ @abstractmethod
+ def output_as_string(
+ self,
+ *,
+ indent: Optional[Union[int, str]] = ...,
+ **kwargs: Any,
+ ) -> str: ...
+
+
+def make_outputter(
+ bom: Bom,
+ output_format: OutputFormat,
+ schema_version: SchemaVersion,
+) -> BaseOutput: ...
diff --git a/typestubs/py_serializable/__init__.pyi
b/typestubs/py_serializable/__init__.pyi
new file mode 100644
index 00000000..da458fc6
--- /dev/null
+++ b/typestubs/py_serializable/__init__.pyi
@@ -0,0 +1,94 @@
+from collections.abc import Callable
+from enum import Enum
+from io import TextIOBase
+from typing import Any, Iterable, Self, TypeVar, overload
+from xml.etree.ElementTree import Element
+
+_T = TypeVar("_T")
+_E = TypeVar("_E")
+_F = TypeVar("_F", bound=Callable[..., Any])
+
+
+class ViewType: ...
+
+
+class SerializationType(str, Enum):
+ JSON = "JSON"
+ XML = "XML"
+
+
+class XmlArraySerializationType(Enum): ...
+
+
+class XmlStringSerializationType(Enum): ...
+
+
+class _JsonSerializable:
+ @classmethod
+ def from_json(cls, data: dict[str, Any]) -> Self | None: ...
+ def as_json(self, view_: type[ViewType] | None = ...) -> str: ...
+
+
+class _XmlSerializable:
+ @classmethod
+ def from_xml(
+ cls,
+ data: TextIOBase | Element,
+ default_namespace: str | None = ...,
+ ) -> Self | None: ...
+ def as_xml(
+ self,
+ view_: type[ViewType] | None = ...,
+ as_string: bool = ...,
+ element_name: str | None = ...,
+ xmlns: str | None = ...,
+ ) -> Element | str: ...
+
+
+class ObjectMetadataLibrary: ...
+
+
+# Typed as a no-op to avoid the Union[Type[_T], Type[_JsonSerializable],
Type[_XmlSerializable]]
+# that py_serializable produces using Union as a stand-in for the unsupported
Intersection type.
+# The serialization methods are added to individual classes via their own
stubs.
+@overload
+def serializable_class(
+ cls: None = ...,
+ *,
+ name: str | None = ...,
+ serialization_types: Iterable[SerializationType] | None = ...,
+ ignore_during_deserialization: Iterable[str] | None = ...,
+ ignore_unknown_during_deserialization: bool = ...,
+) -> Callable[[type[_T]], type[_T]]: ...
+@overload
+def serializable_class(
+ cls: type[_T],
+ *,
+ name: str | None = ...,
+ serialization_types: Iterable[SerializationType] | None = ...,
+ ignore_during_deserialization: Iterable[str] | None = ...,
+ ignore_unknown_during_deserialization: bool = ...,
+) -> type[_T]: ...
+
+
+@overload
+def serializable_enum(cls: None = ...) -> Callable[[type[_E]], type[_E]]: ...
+@overload
+def serializable_enum(cls: type[_E]) -> type[_E]: ...
+
+
+def type_mapping(type_: type) -> Callable[[_F], _F]: ...
+def include_none(
+ view_: type[ViewType] | None = ..., none_value: Any | None = ...
+) -> Callable[[_F], _F]: ...
+def json_name(name: str) -> Callable[[_F], _F]: ...
+def string_format(format_: str) -> Callable[[_F], _F]: ...
+def view(view_: type[ViewType]) -> Callable[[_F], _F]: ...
+def xml_attribute() -> Callable[[_F], _F]: ...
+def xml_array(
+ array_type: XmlArraySerializationType, child_name: str
+) -> Callable[[_F], _F]: ...
+def xml_string(string_type: XmlStringSerializationType) -> Callable[[_F], _F]:
...
+def xml_name(name: str) -> Callable[[_F], _F]: ...
+def xml_sequence(sequence: int) -> Callable[[_F], _F]: ...
+def allow_none_value_for_dict(key: str) -> Callable[[_F], _F]: ...
diff --git a/uv.lock b/uv.lock
index 7240d59c..5d005be2 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,7 +3,7 @@ revision = 3
requires-python = "==3.13.*"
[options]
-exclude-newer = "2026-03-25T17:17:12Z"
+exclude-newer = "2026-03-30T10:05:57Z"
[[package]]
name = "aiofiles"
@@ -25,7 +25,7 @@ wheels = [
[[package]]
name = "aiohttp"
-version = "3.13.3"
+version = "3.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -36,25 +36,25 @@ dependencies = [
{ name = "propcache" },
{ name = "yarl" },
]
-sdist = { url =
"https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz",
hash =
"sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size
= 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
+sdist = { url =
"https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz",
hash =
"sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size
= 7859748, upload-time = "2026-03-28T17:19:40.6Z" }
wheels = [
- { url =
"https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl",
hash =
"sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size
= 734190, upload-time = "2026-01-03T17:30:45.832Z" },
- { url =
"https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl",
hash =
"sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size
= 491783, upload-time = "2026-01-03T17:30:47.466Z" },
- { url =
"https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl",
hash =
"sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size
= 490704, upload-time = "2026-01-03T17:30:49.373Z" },
- { url =
"https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",
hash =
"sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size
= 1720652, upload-time = "2026-01-03T17:30:50.974Z" },
- { url =
"https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",
hash =
"sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size
= 1692014, upload-time = "2026-01-03T17:30:52.729Z" },
- { url =
"https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",
hash =
"sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size
= 1759777, upload-time = "2026-01-03T17:30:54.537Z" },
- { url =
"https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",
hash =
"sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size
= 1861276, upload-time = "2026-01-03T17:30:56.512Z" },
- { url =
"https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
hash =
"sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size
= 1743131, upload-time = "2026-01-03T17:30:58.256Z" },
- { url =
"https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",
hash =
"sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size
= 1556863, upload-time = "2026-01-03T17:31:00.445Z" },
- { url =
"https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl",
hash =
"sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size
= 1682793, upload-time = "2026-01-03T17:31:03.024Z" },
- { url =
"https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl",
hash =
"sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size
= 1716676, upload-time = "2026-01-03T17:31:04.842Z" },
- { url =
"https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl",
hash =
"sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size
= 1733217, upload-time = "2026-01-03T17:31:06.868Z" },
- { url =
"https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl",
hash =
"sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size
= 1552303, upload-time = "2026-01-03T17:31:08.958Z" },
- { url =
"https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl",
hash =
"sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size
= 1763673, upload-time = "2026-01-03T17:31:10.676Z" },
- { url =
"https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl",
hash =
"sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size
= 1721120, upload-time = "2026-01-03T17:31:12.575Z" },
- { url =
"https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl",
hash =
"sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size
= 427383, upload-time = "2026-01-03T17:31:14.382Z" },
- { url =
"https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl",
hash =
"sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size
= 453899, upload-time = "2026-01-03T17:31:15.958Z" },
+ { url =
"https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl",
hash =
"sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size
= 745744, upload-time = "2026-03-28T17:16:44.67Z" },
+ { url =
"https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl",
hash =
"sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size
= 498178, upload-time = "2026-03-28T17:16:46.766Z" },
+ { url =
"https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl",
hash =
"sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size
= 498331, upload-time = "2026-03-28T17:16:48.9Z" },
+ { url =
"https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",
hash =
"sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size
= 1744414, upload-time = "2026-03-28T17:16:50.968Z" },
+ { url =
"https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl",
hash =
"sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size
= 1719226, upload-time = "2026-03-28T17:16:53.173Z" },
+ { url =
"https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",
hash =
"sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size
= 1782110, upload-time = "2026-03-28T17:16:55.362Z" },
+ { url =
"https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",
hash =
"sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size
= 1884809, upload-time = "2026-03-28T17:16:57.734Z" },
+ { url =
"https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
hash =
"sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size
= 1764938, upload-time = "2026-03-28T17:17:00.221Z" },
+ { url =
"https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",
hash =
"sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size
= 1570697, upload-time = "2026-03-28T17:17:02.985Z" },
+ { url =
"https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl",
hash =
"sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size
= 1702258, upload-time = "2026-03-28T17:17:05.28Z" },
+ { url =
"https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl",
hash =
"sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size
= 1740287, upload-time = "2026-03-28T17:17:07.712Z" },
+ { url =
"https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl",
hash =
"sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size
= 1753011, upload-time = "2026-03-28T17:17:10.053Z" },
+ { url =
"https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl",
hash =
"sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size
= 1566359, upload-time = "2026-03-28T17:17:12.433Z" },
+ { url =
"https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl",
hash =
"sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size
= 1785537, upload-time = "2026-03-28T17:17:14.721Z" },
+ { url =
"https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl",
hash =
"sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size
= 1740752, upload-time = "2026-03-28T17:17:17.387Z" },
+ { url =
"https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl",
hash =
"sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size
= 433187, upload-time = "2026-03-28T17:17:19.523Z" },
+ { url =
"https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl",
hash =
"sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size
= 459778, upload-time = "2026-03-28T17:17:21.964Z" },
]
[[package]]
@@ -175,7 +175,7 @@ wheels = [
[[package]]
name = "asfquart"
version = "0.1.13"
-source = { git =
"https://github.com/apache/infrastructure-asfquart.git?rev=main#788bbbcca5552644ed669cd0bbd4bee295da8f9a"
}
+source = { git =
"https://github.com/apache/infrastructure-asfquart.git?rev=main#fab20db357dd689ae5863c86d9eb1b07ae690b83"
}
dependencies = [
{ name = "aiohttp" },
{ name = "asfpy" },
@@ -377,41 +377,41 @@ wheels = [
[[package]]
name = "cryptography"
-version = "46.0.5"
+version = "46.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url =
"https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz",
hash =
"sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size
= 750064, upload-time = "2026-02-10T19:18:38.255Z" }
-wheels = [
- { url =
"https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl",
hash =
"sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size
= 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
- { url =
"https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",
hash =
"sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size
= 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
- { url =
"https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
hash =
"sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size
= 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
- { url =
"https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl",
hash =
"sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size
= 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
- { url =
"https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl",
hash =
"sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size
= 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
- { url =
"https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl",
hash =
"sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size
= 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
- { url =
"https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl",
hash =
"sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size
= 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
- { url =
"https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl",
hash =
"sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size
= 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
- { url =
"https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl",
hash =
"sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size
= 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
- { url =
"https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl",
hash =
"sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size
= 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
- { url =
"https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl",
hash =
"sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size
= 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
- { url =
"https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl",
hash =
"sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size
= 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
- { url =
"https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl",
hash =
"sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size
= 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
- { url =
"https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl",
hash =
"sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size
= 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
- { url =
"https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl",
hash =
"sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size
= 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
- { url =
"https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",
hash =
"sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size
= 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
- { url =
"https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
hash =
"sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size
= 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
- { url =
"https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl",
hash =
"sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size
= 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
- { url =
"https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl",
hash =
"sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size
= 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
- { url =
"https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl",
hash =
"sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size
= 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
- { url =
"https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl",
hash =
"sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size
= 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
- { url =
"https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl",
hash =
"sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size
= 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
- { url =
"https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl",
hash =
"sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size
= 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
- { url =
"https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl",
hash =
"sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size
= 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
- { url =
"https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl",
hash =
"sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size
= 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
- { url =
"https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl",
hash =
"sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size
= 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
- { url =
"https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl",
hash =
"sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size
= 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
- { url =
"https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl",
hash =
"sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size
= 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
+sdist = { url =
"https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz",
hash =
"sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size
= 750542, upload-time = "2026-03-25T23:34:53.396Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl",
hash =
"sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size
= 7176401, upload-time = "2026-03-25T23:33:22.096Z" },
+ { url =
"https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",
hash =
"sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size
= 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
+ { url =
"https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
hash =
"sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size
= 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
+ { url =
"https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl",
hash =
"sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size
= 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
+ { url =
"https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl",
hash =
"sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size
= 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
+ { url =
"https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl",
hash =
"sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size
= 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
+ { url =
"https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl",
hash =
"sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size
= 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
+ { url =
"https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl",
hash =
"sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size
= 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
+ { url =
"https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl",
hash =
"sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size
= 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
+ { url =
"https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl",
hash =
"sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size
= 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
+ { url =
"https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl",
hash =
"sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size
= 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
+ { url =
"https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl",
hash =
"sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size
= 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
+ { url =
"https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl",
hash =
"sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size
= 3026348, upload-time = "2026-03-25T23:33:45.021Z" },
+ { url =
"https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl",
hash =
"sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size
= 3483896, upload-time = "2026-03-25T23:33:46.649Z" },
+ { url =
"https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl",
hash =
"sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size
= 7162776, upload-time = "2026-03-25T23:34:13.308Z" },
+ { url =
"https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",
hash =
"sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size
= 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
+ { url =
"https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
hash =
"sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size
= 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
+ { url =
"https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl",
hash =
"sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size
= 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
+ { url =
"https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl",
hash =
"sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size
= 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
+ { url =
"https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl",
hash =
"sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size
= 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
+ { url =
"https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl",
hash =
"sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size
= 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
+ { url =
"https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl",
hash =
"sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size
= 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
+ { url =
"https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl",
hash =
"sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size
= 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
+ { url =
"https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl",
hash =
"sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size
= 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
+ { url =
"https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl",
hash =
"sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size
= 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
+ { url =
"https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl",
hash =
"sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size
= 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
+ { url =
"https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl",
hash =
"sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size
= 3010660, upload-time = "2026-03-25T23:34:35.418Z" },
+ { url =
"https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl",
hash =
"sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size
= 3471160, upload-time = "2026-03-25T23:34:37.191Z" },
]
[[package]]
@@ -582,16 +582,16 @@ wheels = [
[[package]]
name = "exarch"
-version = "0.2.8"
+version = "0.2.9"
source = { registry = "https://pypi.org/simple" }
wheels = [
- { url =
"https://files.pythonhosted.org/packages/0e/7d/e1a9c9967aac5cfe52c29c664fe0cf2b077edafc1b644d7e3a41464611ee/exarch-0.2.8-cp39-abi3-macosx_10_12_x86_64.whl",
hash =
"sha256:5db6ca025e5af3434253340f10c981dd3da3cce18dd762a7d48d3cb034feb8ad", size
= 1236442, upload-time = "2026-03-15T02:48:20.278Z" },
- { url =
"https://files.pythonhosted.org/packages/bd/76/a4d0cff31e57a7bbfd8ed67f80c64ffe8fd1f3a4323c7907568a8790a6bf/exarch-0.2.8-cp39-abi3-macosx_11_0_arm64.whl",
hash =
"sha256:063a613713c74701be18604381ecf8e1f0266919c20b34fc05dae8ef5a405ca0", size
= 1091798, upload-time = "2026-03-15T02:48:22.721Z" },
- { url =
"https://files.pythonhosted.org/packages/3d/23/fac2da7f9876c249a895934a1522f963819172b046092733843a388dd2d0/exarch-0.2.8-cp39-abi3-manylinux_2_34_aarch64.whl",
hash =
"sha256:d9a123f08828aa08f504c06f22d2d2c7e40b456845029d80a18f847ace44aac0", size
= 1300032, upload-time = "2026-03-15T02:48:24.675Z" },
- { url =
"https://files.pythonhosted.org/packages/81/85/87b658a6f0ed7de1e7b369f8c565255cc1e6da2b86083c9238837ed72bda/exarch-0.2.8-cp39-abi3-manylinux_2_34_x86_64.whl",
hash =
"sha256:397866e49e5113fc13583121df17604ee6eb4cb3dc5ba16b9bb6a54562d379f4", size
= 1389152, upload-time = "2026-03-15T02:48:26.672Z" },
- { url =
"https://files.pythonhosted.org/packages/49/8e/86e5f22f6432608353a64174df155a7c53f6e006401d7c5c49a88d6978a7/exarch-0.2.8-cp39-abi3-musllinux_1_2_aarch64.whl",
hash =
"sha256:0bcc86d4527b9b5f979ac50e8827cf1fd24376e7865877ddc3fea59bead3f56b", size
= 1471165, upload-time = "2026-03-15T02:48:28.51Z" },
- { url =
"https://files.pythonhosted.org/packages/56/1e/a300b1655cd7f3bc942fa78f71248b80275f4e0b85f42cda07e475a8ff05/exarch-0.2.8-cp39-abi3-musllinux_1_2_x86_64.whl",
hash =
"sha256:95ec3fecd5668eb075e9b06bf06c8d163fb1d4f53923cf5b9ad6dbe0891a5f13", size
= 1595388, upload-time = "2026-03-15T02:48:30.252Z" },
- { url =
"https://files.pythonhosted.org/packages/c5/c8/9c95dc56017d0dbecea62758da185bcdbf1c458ea2c7f30bc8df6809f5ab/exarch-0.2.8-cp39-abi3-win_amd64.whl",
hash =
"sha256:ed7f96310997f7459f856708fc4bbaa6e55b4b50801097d2e78343b284762426", size
= 1067310, upload-time = "2026-03-15T02:48:31.624Z" },
+ { url =
"https://files.pythonhosted.org/packages/6a/70/171a44903d8aa2332e3d6d55e6a7597ea4c5941ed8325f2a1449635ac31c/exarch-0.2.9-cp39-abi3-macosx_10_12_x86_64.whl",
hash =
"sha256:eac426747b38a8abe6a36a189bdcdd055950679f669fbf0ddf310c46eb3513db", size
= 1246226, upload-time = "2026-03-25T21:58:10.42Z" },
+ { url =
"https://files.pythonhosted.org/packages/42/fc/2f809e3b373548ad298bbb7bd97c592d0a6dbae2abc6b4220a7993bdfede/exarch-0.2.9-cp39-abi3-macosx_11_0_arm64.whl",
hash =
"sha256:506697e367450a5ffafb16b12d3c4da64671766eca687996d206e9d6e4b7f53a", size
= 1099968, upload-time = "2026-03-25T21:58:12.294Z" },
+ { url =
"https://files.pythonhosted.org/packages/f2/54/a32835a445dcde56ffaf9826e8ad7738fa8ec48a87ab22ddd92561245cdb/exarch-0.2.9-cp39-abi3-manylinux_2_34_aarch64.whl",
hash =
"sha256:94e1cdd48f7668f97380b8507df2f8e87b0c801ee573a7f976cdd93576be522a", size
= 1307715, upload-time = "2026-03-25T21:58:13.716Z" },
+ { url =
"https://files.pythonhosted.org/packages/ba/37/b39ecd16eadb81cdd7266660a25409e751bb2779850b4f4e1c95cbcfd7cf/exarch-0.2.9-cp39-abi3-manylinux_2_34_x86_64.whl",
hash =
"sha256:41cf45de8bdf102c7f70f98ab0a55ad657ca3f1df1a0bca937734edd2ac90fec", size
= 1399745, upload-time = "2026-03-25T21:58:15.134Z" },
+ { url =
"https://files.pythonhosted.org/packages/0b/a0/70711986b43590db3e344644d47605f57dab5cc91cf2c19948f4a909fd55/exarch-0.2.9-cp39-abi3-musllinux_1_2_aarch64.whl",
hash =
"sha256:2ee98071e4e51cbb5640b02a0286cca938ba272d6ef2c3db18393a71e7c35c7a", size
= 1478421, upload-time = "2026-03-25T21:58:16.535Z" },
+ { url =
"https://files.pythonhosted.org/packages/ae/bc/12823a091551215f41cbdb713c52332a1ea418686280458349f66e87a61e/exarch-0.2.9-cp39-abi3-musllinux_1_2_x86_64.whl",
hash =
"sha256:ffd98180cf397338f752f281f6c85ca1989822809fbe87dd5aea73f6971e1f54", size
= 1605251, upload-time = "2026-03-25T21:58:17.985Z" },
+ { url =
"https://files.pythonhosted.org/packages/2e/40/0eeab4d53394a3915888f78ad5cd66615e42b5dfaf67ab83594fe219ca46/exarch-0.2.9-cp39-abi3-win_amd64.whl",
hash =
"sha256:594889809eee16eeeb50752234eef50c74d760bf94a9631fa96a2229680f39a8", size
= 1076429, upload-time = "2026-03-25T21:58:19.587Z" },
]
[[package]]
@@ -867,11 +867,11 @@ wheels = [
[[package]]
name = "json5"
-version = "0.13.0"
+version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url =
"https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz",
hash =
"sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size
= 52441, upload-time = "2026-01-01T19:42:14.99Z" }
+sdist = { url =
"https://files.pythonhosted.org/packages/9c/4b/6f8906aaf67d501e259b0adab4d312945bb7211e8b8d4dcc77c92320edaa/json5-0.14.0.tar.gz",
hash =
"sha256:b3f492fad9f6cdbced8b7d40b28b9b1c9701c5f561bef0d33b81c2ff433fefcb", size
= 52656, upload-time = "2026-03-27T22:50:48.108Z" }
wheels = [
- { url =
"https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl",
hash =
"sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size
= 36163, upload-time = "2026-01-01T19:42:13.962Z" },
+ { url =
"https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl",
hash =
"sha256:56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a", size
= 36271, upload-time = "2026-03-27T22:50:47.073Z" },
]
[[package]]
@@ -1376,11 +1376,11 @@ wheels = [
[[package]]
name = "pygments"
-version = "2.19.2"
+version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url =
"https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz",
hash =
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size
= 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+sdist = { url =
"https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz",
hash =
"sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size
= 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
- { url =
"https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl",
hash =
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size
= 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+ { url =
"https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl",
hash =
"sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size
= 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
@@ -1493,15 +1493,15 @@ wheels = [
[[package]]
name = "python-discovery"
-version = "1.2.0"
+version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
-sdist = { url =
"https://files.pythonhosted.org/packages/9c/90/bcce6b46823c9bec1757c964dc37ed332579be512e17a30e9698095dcae4/python_discovery-1.2.0.tar.gz",
hash =
"sha256:7d33e350704818b09e3da2bd419d37e21e7c30db6e0977bb438916e06b41b5b1", size
= 58055, upload-time = "2026-03-19T01:43:08.248Z" }
+sdist = { url =
"https://files.pythonhosted.org/packages/b9/88/815e53084c5079a59df912825a279f41dd2e0df82281770eadc732f5352c/python_discovery-1.2.1.tar.gz",
hash =
"sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e", size
= 58457, upload-time = "2026-03-26T22:30:44.496Z" }
wheels = [
- { url =
"https://files.pythonhosted.org/packages/c2/3c/2005227cb951df502412de2fa781f800663cccbef8d90ec6f1b371ac2c0d/python_discovery-1.2.0-py3-none-any.whl",
hash =
"sha256:1e108f1bbe2ed0ef089823d28805d5ad32be8e734b86a5f212bf89b71c266e4a", size
= 31524, upload-time = "2026-03-19T01:43:07.045Z" },
+ { url =
"https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl",
hash =
"sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502", size
= 31674, upload-time = "2026-03-26T22:30:43.396Z" },
]
[[package]]
@@ -1636,42 +1636,42 @@ wheels = [
[[package]]
name = "regex"
-version = "2026.2.28"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url =
"https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz",
hash =
"sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size
= 415184, upload-time = "2026-02-28T02:19:42.792Z" }
-wheels = [
- { url =
"https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl",
hash =
"sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size
= 489311, upload-time = "2026-02-28T02:17:22.591Z" },
- { url =
"https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl",
hash =
"sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size
= 291285, upload-time = "2026-02-28T02:17:24.355Z" },
- { url =
"https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl",
hash =
"sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size
= 289051, upload-time = "2026-02-28T02:17:26.722Z" },
- { url =
"https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",
hash =
"sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size
= 796842, upload-time = "2026-02-28T02:17:29.064Z" },
- { url =
"https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",
hash =
"sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size
= 863083, upload-time = "2026-02-28T02:17:31.363Z" },
- { url =
"https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",
hash =
"sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size
= 909412, upload-time = "2026-02-28T02:17:33.248Z" },
- { url =
"https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
hash =
"sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size
= 802101, upload-time = "2026-02-28T02:17:35.053Z" },
- { url =
"https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",
hash =
"sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size
= 775260, upload-time = "2026-02-28T02:17:37.692Z" },
- { url =
"https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl",
hash =
"sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size
= 784311, upload-time = "2026-02-28T02:17:39.855Z" },
- { url =
"https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl",
hash =
"sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size
= 856876, upload-time = "2026-02-28T02:17:42.317Z" },
- { url =
"https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl",
hash =
"sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size
= 763632, upload-time = "2026-02-28T02:17:45.073Z" },
- { url =
"https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl",
hash =
"sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size
= 849320, upload-time = "2026-02-28T02:17:47.192Z" },
- { url =
"https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl",
hash =
"sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size
= 790152, upload-time = "2026-02-28T02:17:49.067Z" },
- { url =
"https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl",
hash =
"sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size
= 266398, upload-time = "2026-02-28T02:17:50.744Z" },
- { url =
"https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl",
hash =
"sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size
= 277282, upload-time = "2026-02-28T02:17:53.074Z" },
- { url =
"https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl",
hash =
"sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size
= 270382, upload-time = "2026-02-28T02:17:54.888Z" },
- { url =
"https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl",
hash =
"sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size
= 492541, upload-time = "2026-02-28T02:17:56.813Z" },
- { url =
"https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl",
hash =
"sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size
= 292984, upload-time = "2026-02-28T02:17:58.538Z" },
- { url =
"https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl",
hash =
"sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size
= 291509, upload-time = "2026-02-28T02:18:00.208Z" },
- { url =
"https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",
hash =
"sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size
= 809429, upload-time = "2026-02-28T02:18:02.328Z" },
- { url =
"https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",
hash =
"sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size
= 869422, upload-time = "2026-02-28T02:18:04.23Z" },
- { url =
"https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",
hash =
"sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size
= 915175, upload-time = "2026-02-28T02:18:06.791Z" },
- { url =
"https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
hash =
"sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size
= 812044, upload-time = "2026-02-28T02:18:08.736Z" },
- { url =
"https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",
hash =
"sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size
= 782056, upload-time = "2026-02-28T02:18:10.777Z" },
- { url =
"https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl",
hash =
"sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size
= 798743, upload-time = "2026-02-28T02:18:13.025Z" },
- { url =
"https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl",
hash =
"sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size
= 864633, upload-time = "2026-02-28T02:18:16.84Z" },
- { url =
"https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl",
hash =
"sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size
= 770862, upload-time = "2026-02-28T02:18:18.892Z" },
- { url =
"https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl",
hash =
"sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size
= 854788, upload-time = "2026-02-28T02:18:21.475Z" },
- { url =
"https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl",
hash =
"sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size
= 800184, upload-time = "2026-02-28T02:18:23.492Z" },
- { url =
"https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl",
hash =
"sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size
= 269137, upload-time = "2026-02-28T02:18:25.375Z" },
- { url =
"https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl",
hash =
"sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size
= 280682, upload-time = "2026-02-28T02:18:27.205Z" },
- { url =
"https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl",
hash =
"sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size
= 271735, upload-time = "2026-02-28T02:18:29.015Z" },
+version = "2026.3.32"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url =
"https://files.pythonhosted.org/packages/81/93/5ab3e899c47fa7994e524447135a71cd121685a35c8fe35029005f8b236f/regex-2026.3.32.tar.gz",
hash =
"sha256:f1574566457161678297a116fa5d1556c5a4159d64c5ff7c760e7c564bf66f16", size
= 415605, upload-time = "2026-03-28T21:49:22.012Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/bd/ba/9c1819f302b42b5fbd4139ead6280e9ec37d19bbe33379df0039b2a57bb4/regex-2026.3.32-cp313-cp313-macosx_10_13_universal2.whl",
hash =
"sha256:c6d9c6e783b348f719b6118bb3f187b2e138e3112576c9679eb458cc8b2e164b", size
= 490394, upload-time = "2026-03-28T21:46:58.112Z" },
+ { url =
"https://files.pythonhosted.org/packages/5b/0b/f62b0ce79eb83ca82fffea1736289d29bc24400355968301406789bcebd2/regex-2026.3.32-cp313-cp313-macosx_10_13_x86_64.whl",
hash =
"sha256:0f21ae18dfd15752cdd98d03cbd7a3640be826bfd58482a93f730dbd24d7b9fb", size
= 291993, upload-time = "2026-03-28T21:47:00.198Z" },
+ { url =
"https://files.pythonhosted.org/packages/e7/d8/ba0f8f81f88cd20c0b27acc123561ac5495ea33f800f0b8ebed2038b23eb/regex-2026.3.32-cp313-cp313-macosx_11_0_arm64.whl",
hash =
"sha256:844d88509c968dd44b30daeefac72b038b1bf31ac372d5106358ab01d393c48b", size
= 289618, upload-time = "2026-03-28T21:47:02.269Z" },
+ { url =
"https://files.pythonhosted.org/packages/fd/0d/b47a0e68bc511c195ff129c0311a4cd79b954b8676193a9d03a97c623a91/regex-2026.3.32-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",
hash =
"sha256:8fc918cd003ba0d066bf0003deb05a259baaaab4dc9bd4f1207bbbe64224857a", size
= 796427, upload-time = "2026-03-28T21:47:04.096Z" },
+ { url =
"https://files.pythonhosted.org/packages/51/d7/32b05aa8fde7789ba316533c0f30e87b6b5d38d6d7f8765eadc5aab84671/regex-2026.3.32-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",
hash =
"sha256:bbc458a292aee57d572075f22c035fa32969cdb7987d454e3e34d45a40a0a8b4", size
= 865850, upload-time = "2026-03-28T21:47:05.982Z" },
+ { url =
"https://files.pythonhosted.org/packages/dc/67/828d8095501f237b83f630d4069eea8c0e5cb6a204e859cf0b67c223ce12/regex-2026.3.32-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",
hash =
"sha256:987cdfcfb97a249abc3601ad53c7de5c370529f1981e4c8c46793e4a1e1bfe8e", size
= 913578, upload-time = "2026-03-28T21:47:08.172Z" },
+ { url =
"https://files.pythonhosted.org/packages/0f/f8/acf1eb80f58852e85bd39a6ddfa78ce2243ddc8de8da7582e6ba657da593/regex-2026.3.32-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
hash =
"sha256:a5d88fa37ba5e8a80ca8d956b9ea03805cfa460223ac94b7d4854ee5e30f3173", size
= 801536, upload-time = "2026-03-28T21:47:10.206Z" },
+ { url =
"https://files.pythonhosted.org/packages/9f/05/986cdf8d12693451f5889aaf4ea4f65b2c49b1152ae814fa1fb75439e40b/regex-2026.3.32-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",
hash =
"sha256:4d082be64e51671dd5ee1c208c92da2ddda0f2f20d8ef387e57634f7e97b6aae", size
= 776226, upload-time = "2026-03-28T21:47:12.891Z" },
+ { url =
"https://files.pythonhosted.org/packages/32/02/945a6a2348ca1c6608cb1747275c8affd2ccd957d4885c25218a86377912/regex-2026.3.32-cp313-cp313-musllinux_1_2_aarch64.whl",
hash =
"sha256:c1d7fa44aece1fa02b8927441614c96520253a5cad6a96994e3a81e060feed55", size
= 785933, upload-time = "2026-03-28T21:47:14.795Z" },
+ { url =
"https://files.pythonhosted.org/packages/53/12/c5bab6cc679ad79a45427a98c4e70809586ac963c5ad54a9217533c4763e/regex-2026.3.32-cp313-cp313-musllinux_1_2_ppc64le.whl",
hash =
"sha256:d478a2ca902b6ef28ffc9521e5f0f728d036abe35c0b250ee8ae78cfe7c5e44e", size
= 860671, upload-time = "2026-03-28T21:47:16.985Z" },
+ { url =
"https://files.pythonhosted.org/packages/bf/68/8d85f98c2443469facabef62b82b851d369b13f92bec2ca7a3808deaa47b/regex-2026.3.32-cp313-cp313-musllinux_1_2_riscv64.whl",
hash =
"sha256:2820d2231885e97aff0fcf230a19ebd5d2b5b8a1ba338c20deb34f16db1c7897", size
= 765335, upload-time = "2026-03-28T21:47:18.872Z" },
+ { url =
"https://files.pythonhosted.org/packages/89/a7/d8a9c270916107a501fca63b748547c6c77e570d19f16a29b557ce734f3d/regex-2026.3.32-cp313-cp313-musllinux_1_2_s390x.whl",
hash =
"sha256:fc8ced733d6cd9af5e412f256a32f7c61cd2d7371280a65c689939ac4572499f", size
= 851913, upload-time = "2026-03-28T21:47:20.793Z" },
+ { url =
"https://files.pythonhosted.org/packages/f4/8e/03d392b26679914ccf21f83d18ad4443232d2f8c3e2c30a962d4e3918d9c/regex-2026.3.32-cp313-cp313-musllinux_1_2_x86_64.whl",
hash =
"sha256:847087abe98b3c1ebf1eb49d6ef320dbba75a83ee4f83c94704580f1df007dd4", size
= 788447, upload-time = "2026-03-28T21:47:22.628Z" },
+ { url =
"https://files.pythonhosted.org/packages/cf/df/692227d23535a50604333068b39eb262626db780ab1e1b19d83fc66853aa/regex-2026.3.32-cp313-cp313-win32.whl",
hash =
"sha256:d21a07edddb3e0ca12a8b8712abc8452481c3d3db19ae87fc94e9842d005964b", size
= 266834, upload-time = "2026-03-28T21:47:24.778Z" },
+ { url =
"https://files.pythonhosted.org/packages/b9/37/13e4e56adc16ba607cffa1fe880f233eb9ded8ab8a8580619683c9e4ce48/regex-2026.3.32-cp313-cp313-win_amd64.whl",
hash =
"sha256:3c054e39a9f85a3d76c62a1d50c626c5e9306964eaa675c53f61ff7ec1204bbb", size
= 277972, upload-time = "2026-03-28T21:47:26.627Z" },
+ { url =
"https://files.pythonhosted.org/packages/ab/1c/80a86dbb2b416fec003b1801462bdcebbf1d43202ed5acb176e99c1ba369/regex-2026.3.32-cp313-cp313-win_arm64.whl",
hash =
"sha256:b2e9c2ea2e93223579308263f359eab8837dc340530b860cb59b713651889f14", size
= 270649, upload-time = "2026-03-28T21:47:28.551Z" },
+ { url =
"https://files.pythonhosted.org/packages/58/08/e38372da599dc1c39c599907ec535016d110034bd3701ce36554f59767ef/regex-2026.3.32-cp313-cp313t-macosx_10_13_universal2.whl",
hash =
"sha256:5d86e3fb08c94f084a625c8dc2132a79a3a111c8bf6e2bc59351fa61753c2f6e", size
= 494495, upload-time = "2026-03-28T21:47:30.642Z" },
+ { url =
"https://files.pythonhosted.org/packages/5f/27/6e29ece8c9ce01001ece1137fa21c8707529c2305b22828f63623b0eb262/regex-2026.3.32-cp313-cp313t-macosx_10_13_x86_64.whl",
hash =
"sha256:b6f366a5ef66a2df4d9e68035cfe9f0eb8473cdfb922c37fac1d169b468607b0", size
= 293988, upload-time = "2026-03-28T21:47:32.553Z" },
+ { url =
"https://files.pythonhosted.org/packages/e1/98/8752e18bb87a2fe728b73b0f83c082eb162a470766063f8028759fb26844/regex-2026.3.32-cp313-cp313t-macosx_11_0_arm64.whl",
hash =
"sha256:b8fca73e16c49dd972ce3a88278dfa5b93bf91ddef332a46e9443abe21ca2f7c", size
= 292634, upload-time = "2026-03-28T21:47:34.651Z" },
+ { url =
"https://files.pythonhosted.org/packages/7f/7b/d7729fe294e23e9c7c3871cb69d49059fa7d65fd11e437a2cbea43f6615d/regex-2026.3.32-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",
hash =
"sha256:b953d9d496d19786f4d46e6ba4b386c6e493e81e40f9c5392332458183b0599d", size
= 810532, upload-time = "2026-03-28T21:47:36.839Z" },
+ { url =
"https://files.pythonhosted.org/packages/fd/49/4dae7b000659f611b17b9c1541fba800b0569e4060debc4635ef1b23982c/regex-2026.3.32-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl",
hash =
"sha256:b565f25171e04d4fad950d1fa837133e3af6ea6f509d96166eed745eb0cf63bc", size
= 871919, upload-time = "2026-03-28T21:47:39.192Z" },
+ { url =
"https://files.pythonhosted.org/packages/83/85/aa8ad3977b9399861db3df62b33fe5fef6932ee23a1b9f4f357f58f2094b/regex-2026.3.32-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl",
hash =
"sha256:f28eac18a8733a124444643a66ac96fef2c0ad65f50034e0a043b90333dc677f", size
= 916550, upload-time = "2026-03-28T21:47:41.618Z" },
+ { url =
"https://files.pythonhosted.org/packages/c8/c0/6379d7f5b59ff0656ba49cf666d5013ecee55e83245275b310b0ffc79143/regex-2026.3.32-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
hash =
"sha256:7cdd508664430dd51b8888deb6c5b416d8de046b2e11837254378d31febe4a98", size
= 814988, upload-time = "2026-03-28T21:47:43.681Z" },
+ { url =
"https://files.pythonhosted.org/packages/2c/af/2dfddc64074bd9b70e27e170ee9db900542e2870210b489ad4471416ba86/regex-2026.3.32-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl",
hash =
"sha256:5c35d097f509cf7e40d20d5bee548d35d6049b36eb9965e8d43e4659923405b9", size
= 786337, upload-time = "2026-03-28T21:47:46.076Z" },
+ { url =
"https://files.pythonhosted.org/packages/eb/2f/4eb8abd705236402b4fe0e130971634deffb1855e2028bf02a2b7c0e841c/regex-2026.3.32-cp313-cp313t-musllinux_1_2_aarch64.whl",
hash =
"sha256:85c9b0c131427470a6423baa0a9330be6fd8c3630cc3ee6fdee03360724cbec5", size
= 800029, upload-time = "2026-03-28T21:47:48.356Z" },
+ { url =
"https://files.pythonhosted.org/packages/3e/2c/77d9ca2c9df483b51b4b1291c96d79c9ae301077841c4db39bc822f6b4c6/regex-2026.3.32-cp313-cp313t-musllinux_1_2_ppc64le.whl",
hash =
"sha256:e50af656c15e2723eeb7279c0837e07accc594b95ec18b86821a4d44b51b24bf", size
= 865843, upload-time = "2026-03-28T21:47:50.762Z" },
+ { url =
"https://files.pythonhosted.org/packages/48/10/306f477a509f4eed699071b1f031d89edd5a2b5fa28c8ede5b2638eaba82/regex-2026.3.32-cp313-cp313t-musllinux_1_2_riscv64.whl",
hash =
"sha256:4bc32b4dbdb4f9f300cf9f38f8ea2ce9511a068ffaa45ac1373ee7a943f1d810", size
= 772473, upload-time = "2026-03-28T21:47:52.771Z" },
+ { url =
"https://files.pythonhosted.org/packages/f4/f6/54bd83ec46ac037de2beb049afc9dd5d2769c6ecaadf7856254ce610e62a/regex-2026.3.32-cp313-cp313t-musllinux_1_2_s390x.whl",
hash =
"sha256:e3e5d1802cba785210a4a800e63fcee7a228649a880f3bf7f2aadccb151a834b", size
= 856805, upload-time = "2026-03-28T21:47:55.04Z" },
+ { url =
"https://files.pythonhosted.org/packages/37/e8/ee0e7d14de1fc6582d5782f072db6c61465a38a4142f88e175dda494b536/regex-2026.3.32-cp313-cp313t-musllinux_1_2_x86_64.whl",
hash =
"sha256:ef250a3f5e93182193f5c927c5e9575b2cb14b80d03e258bc0b89cc5de076b60", size
= 801875, upload-time = "2026-03-28T21:47:57.434Z" },
+ { url =
"https://files.pythonhosted.org/packages/8a/06/0fa9daca59d07b6aabd8e0468d3b86fd578576a157206fbcddbfc2298f7d/regex-2026.3.32-cp313-cp313t-win32.whl",
hash =
"sha256:9cf7036dfa2370ccc8651521fcbb40391974841119e9982fa312b552929e6c85", size
= 269892, upload-time = "2026-03-28T21:47:59.674Z" },
+ { url =
"https://files.pythonhosted.org/packages/13/47/77f16b5ad9f10ca574f03d84a354b359b0ac33f85054f2f2daafc9f7b807/regex-2026.3.32-cp313-cp313t-win_amd64.whl",
hash =
"sha256:c940e00e8d3d10932c929d4b8657c2ea47d2560f31874c3e174c0d3488e8b865", size
= 281318, upload-time = "2026-03-28T21:48:01.562Z" },
+ { url =
"https://files.pythonhosted.org/packages/c6/47/db4446faaea8d01c8315c9c89c7dc6abbb3305e8e712e9b23936095c4d58/regex-2026.3.32-cp313-cp313t-win_arm64.whl",
hash =
"sha256:ace48c5e157c1e58b7de633c5e257285ce85e567ac500c833349c363b3df69d4", size
= 272366, upload-time = "2026-03-28T21:48:03.748Z" },
]
[[package]]
@@ -1774,27 +1774,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.15.7"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url =
"https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz",
hash =
"sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size
= 4601277, upload-time = "2026-03-19T16:26:22.605Z" }
-wheels = [
- { url =
"https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl",
hash =
"sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size
= 10489037, upload-time = "2026-03-19T16:26:32.47Z" },
- { url =
"https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl",
hash =
"sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size
= 10955433, upload-time = "2026-03-19T16:27:00.205Z" },
- { url =
"https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl",
hash =
"sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size
= 10269302, upload-time = "2026-03-19T16:26:26.183Z" },
- { url =
"https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
hash =
"sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size
= 10607625, upload-time = "2026-03-19T16:27:03.263Z" },
- { url =
"https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",
hash =
"sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size
= 10324743, upload-time = "2026-03-19T16:27:09.791Z" },
- { url =
"https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl",
hash =
"sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size
= 11138536, upload-time = "2026-03-19T16:27:06.101Z" },
- { url =
"https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
hash =
"sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size
= 11994292, upload-time = "2026-03-19T16:26:48.718Z" },
- { url =
"https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl",
hash =
"sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size
= 11398981, upload-time = "2026-03-19T16:26:54.513Z" },
- { url =
"https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
hash =
"sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size
= 11242422, upload-time = "2026-03-19T16:26:29.277Z" },
- { url =
"https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl",
hash =
"sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size
= 11232158, upload-time = "2026-03-19T16:26:42.321Z" },
- { url =
"https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl",
hash =
"sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size
= 10577861, upload-time = "2026-03-19T16:26:57.459Z" },
- { url =
"https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl",
hash =
"sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size
= 10327310, upload-time = "2026-03-19T16:26:35.909Z" },
- { url =
"https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl",
hash =
"sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size
= 10840752, upload-time = "2026-03-19T16:26:45.723Z" },
- { url =
"https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl",
hash =
"sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size
= 11336961, upload-time = "2026-03-19T16:26:39.076Z" },
- { url =
"https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl",
hash =
"sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size
= 10582538, upload-time = "2026-03-19T16:26:15.992Z" },
- { url =
"https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl",
hash =
"sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size
= 11755839, upload-time = "2026-03-19T16:26:19.897Z" },
- { url =
"https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl",
hash =
"sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size
= 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
+version = "0.15.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url =
"https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz",
hash =
"sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size
= 4610921, upload-time = "2026-03-26T18:39:38.675Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl",
hash =
"sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size
= 10527394, upload-time = "2026-03-26T18:39:41.566Z" },
+ { url =
"https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl",
hash =
"sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size
= 10905693, upload-time = "2026-03-26T18:39:30.364Z" },
+ { url =
"https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl",
hash =
"sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size
= 10323044, upload-time = "2026-03-26T18:39:33.37Z" },
+ { url =
"https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
hash =
"sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size
= 10629135, upload-time = "2026-03-26T18:39:44.142Z" },
+ { url =
"https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",
hash =
"sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size
= 10348041, upload-time = "2026-03-26T18:39:52.178Z" },
+ { url =
"https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl",
hash =
"sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size
= 11121987, upload-time = "2026-03-26T18:39:55.195Z" },
+ { url =
"https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",
hash =
"sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size
= 11951057, upload-time = "2026-03-26T18:39:19.18Z" },
+ { url =
"https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl",
hash =
"sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size
= 11464613, upload-time = "2026-03-26T18:40:06.301Z" },
+ { url =
"https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
hash =
"sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size
= 11257557, upload-time = "2026-03-26T18:39:57.972Z" },
+ { url =
"https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl",
hash =
"sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size
= 11169440, upload-time = "2026-03-26T18:39:22.205Z" },
+ { url =
"https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl",
hash =
"sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size
= 10605963, upload-time = "2026-03-26T18:39:46.682Z" },
+ { url =
"https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl",
hash =
"sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size
= 10357484, upload-time = "2026-03-26T18:39:49.176Z" },
+ { url =
"https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl",
hash =
"sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size
= 10830426, upload-time = "2026-03-26T18:40:03.702Z" },
+ { url =
"https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl",
hash =
"sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size
= 11345125, upload-time = "2026-03-26T18:39:27.799Z" },
+ { url =
"https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl",
hash =
"sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size
= 10571959, upload-time = "2026-03-26T18:39:36.117Z" },
+ { url =
"https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl",
hash =
"sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size
= 11743893, upload-time = "2026-03-26T18:39:25.01Z" },
+ { url =
"https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl",
hash =
"sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size
= 11048175, upload-time = "2026-03-26T18:40:01.06Z" },
]
[[package]]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]