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

commit db9623046b767c92026c59dfb408dc03819743d6
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/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                   |   3 +-
 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                       |  73 ++++++++-
 atr/templates/draft-tools.html          |   8 +
 pip-audit.requirements                  |  18 +--
 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 ++++++++++++++++----------------
 27 files changed, 696 insertions(+), 317 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/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..8bc8d19b 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
@@ -39,7 +40,7 @@ def validate_cli(bundle_value: models.bundle.Bundle) -> 
list[str] | None:
         "validate",
         "--fail-on-errors",
         "--input-format",
-        "json",
+        bundle_value.source_type,
         "--input-file",
         bundle_value.path.as_posix(),
     ]
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..33657e7e 100644
--- a/atr/tasks/sbom.py
+++ b/atr/tasks/sbom.py
@@ -40,6 +40,14 @@ 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."""
 
@@ -47,6 +55,14 @@ class GenerateCycloneDX(schema.Strict):
     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):
     """Custom exception for SBOM generation failures."""
 
@@ -83,12 +99,10 @@ class ScoreArgs(FileArgs):
 
 @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)})
     full_path = base_dir / path_str
@@ -97,6 +111,8 @@ async def augment(args: FileArgs) -> results.Results | None:
         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
@@ -128,6 +144,21 @@ async def augment(args: FileArgs) -> results.Results | 
None:
     )
 
 
[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."""
@@ -159,6 +190,8 @@ async def osv_scan(args: FileArgs) -> results.Results | 
None:
     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 = []
@@ -259,8 +292,10 @@ async def score_tool(args: ScoreArgs) -> results.Results | 
None:
     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)
@@ -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,
     )
 
@@ -325,6 +360,34 @@ 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}")
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/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]

Reply via email to