This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/sbp by this push:
     new ef54d39c Update dependencies and fix style problems
ef54d39c is described below

commit ef54d39c4070f0b5efb3050a09cd43ea248b1db0
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Mar 20 16:47:10 2026 +0000

    Update dependencies and fix style problems
---
 atr/admin/__init__.py         |  28 ++---
 atr/blueprints/admin.py       |   8 +-
 atr/blueprints/api.py         |  20 ++--
 atr/blueprints/common.py      |  10 +-
 atr/blueprints/post.py        |   2 +-
 atr/models/github.py          |   2 +-
 atr/paths.py                  |   8 +-
 atr/ssh.py                    |  34 +++---
 atr/tabulate.py               |   2 +-
 atr/tasks/quarantine.py       |   2 +-
 atr/util.py                   |  44 +++----
 atr/web.py                    |   2 +-
 pip-audit.requirements        |  10 +-
 tests/unit/test_blueprints.py | 264 +++++++++++++++++++++---------------------
 tests/unit/test_detection.py  |  44 +++----
 tests/unit/test_safe_types.py |  12 +-
 uv.lock                       |  76 ++++++------
 17 files changed, 284 insertions(+), 284 deletions(-)

diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index 1ce05888..cca450a1 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -1290,6 +1290,20 @@ async def _delete_releases(session: web.Committer, 
releases_to_delete: list[str]
         await quart.flash(f"Failed to delete {util.plural(fail_count, 
'release')}:\n{errors_str}", "error")
 
 
+async def _fetch_ongoing_tasks(
+    _session: web.Committer,
+    project_key: safe.ProjectKey,
+    version_key: safe.VersionKey,
+    revision: safe.RevisionNumber,
+) -> web.QuartResponse:
+    try:
+        ongoing = await interaction.tasks_ongoing(project_key, version_key, 
revision)
+        return web.TextResponse(str(ongoing))
+    except Exception:
+        log.exception(f"Error fetching ongoing task count for {project_key!s} 
{version_key!s} rev {revision!s}:")
+        return web.TextResponse("")
+
+
 def _format_exception_location(exc: BaseException) -> str:
     tb = exc.__traceback__
     last_tb = None
@@ -1338,20 +1352,6 @@ async def 
_get_filesystem_dirs_unfinished(filesystem_dirs: list[str]) -> None:
                         filesystem_dirs.append(version_dir_path)
 
 
-async def _fetch_ongoing_tasks(
-    _session: web.Committer,
-    project_key: safe.ProjectKey,
-    version_key: safe.VersionKey,
-    revision: safe.RevisionNumber,
-) -> web.QuartResponse:
-    try:
-        ongoing = await interaction.tasks_ongoing(project_key, version_key, 
revision)
-        return web.TextResponse(str(ongoing))
-    except Exception:
-        log.exception(f"Error fetching ongoing task count for {project_key!s} 
{version_key!s} rev {revision!s}:")
-        return web.TextResponse("")
-
-
 def _require_debug_and_allow_tests() -> None:
     conf = config.get()
     debug_and_allow_tests = (config.get_mode() == config.Mode.Debug) and 
conf.ALLOW_TESTS
diff --git a/atr/blueprints/admin.py b/atr/blueprints/admin.py
index 6d2bf7fe..e0a75fcf 100644
--- a/atr/blueprints/admin.py
+++ b/atr/blueprints/admin.py
@@ -80,10 +80,10 @@ def typed(func: Callable[..., Any]) -> Callable[..., Any]:
     path, validated_params, literal_params, body_param, form_param, 
query_param, optional_params = (
         common.build_api_path(func)
     )
-    method = "POST" if (body_param is not None or form_param is not None) else 
"GET"
-    body_safe_params = common.safe_params_for_type(body_param[1]) if 
body_param is not None else []
-    form_safe_params = common.safe_params_for_type(form_param[1]) if 
form_param is not None else []
-    query_safe_params = common.safe_params_for_type(query_param[1]) if 
query_param is not None else []
+    method = "POST" if ((body_param is not None) or (form_param is not None)) 
else "GET"
+    body_safe_params = common.safe_params_for_type(body_param[1]) if 
(body_param is not None) else []
+    form_safe_params = common.safe_params_for_type(form_param[1]) if 
(form_param is not None) else []
+    query_safe_params = common.safe_params_for_type(query_param[1]) if 
(query_param is not None) else []
 
     async def wrapper(*_args: Any, **kwargs: Any) -> Any:
         enhanced_session = await common.authenticate()
diff --git a/atr/blueprints/api.py b/atr/blueprints/api.py
index 172e75ec..facc1374 100644
--- a/atr/blueprints/api.py
+++ b/atr/blueprints/api.py
@@ -61,9 +61,9 @@ def typed(func: Callable[..., Awaitable[Any]]) -> 
web.RouteFunction[Any]:
     path, validated_params, literal_params, body_param, _, query_param, 
optional_params = common.build_api_path(
         original
     )
-    method = "POST" if body_param is not None else "GET"
-    body_safe_params = common.safe_params_for_type(body_param[1]) if 
body_param is not None else []
-    query_safe_params = common.safe_params_for_type(query_param[1]) if 
query_param is not None else []
+    method = "POST" if (body_param is not None) else "GET"
+    body_safe_params = common.safe_params_for_type(body_param[1]) if 
(body_param is not None) else []
+    query_safe_params = common.safe_params_for_type(query_param[1]) if 
(query_param is not None) else []
 
     async def wrapper(*_args: Any, **kwargs: Any) -> Any:
         await common.validate_params(kwargs, validated_params)
@@ -107,13 +107,6 @@ def typed(func: Callable[..., Awaitable[Any]]) -> 
web.RouteFunction[Any]:
     return wrapper
 
 
-@_BLUEPRINT.before_request
-@rate_limiter.rate_limit(500, datetime.timedelta(hours=1))
-async def _api_rate_limit() -> None:
-    """Set API-wide rate limit"""
-    pass
-
-
 def _add_url_rules(
     wrapper: Callable[..., Any],
     path: str,
@@ -134,6 +127,13 @@ def _add_url_rules(
         _BLUEPRINT.add_url_rule(path, endpoint=endpoint, view_func=wrapper, 
methods=[method])
 
 
+@_BLUEPRINT.before_request
+@rate_limiter.rate_limit(500, datetime.timedelta(hours=1))
+async def _api_rate_limit() -> None:
+    """Set API-wide rate limit"""
+    pass
+
+
 def _copy_quart_attributes(src: Callable[..., Any], dst: Callable[..., Any]) 
-> None:
     """Copy quart schema attributes from src to dst to preserve OpenAPI 
documentation."""
     for attr in common.QUART_ATTRIBUTES:
diff --git a/atr/blueprints/common.py b/atr/blueprints/common.py
index c5e26a32..c1b7938f 100644
--- a/atr/blueprints/common.py
+++ b/atr/blueprints/common.py
@@ -278,14 +278,14 @@ async def parse_query(
 
 def _coerce_query_field(raw: str, field_type: Any, field_name: str) -> Any:
     """Coerce a raw query string value to the expected field type."""
-    if field_type is str or field_type == "str":
+    if (field_type is str) or (field_type == "str"):
         return raw
-    if field_type is int or field_type == "int":
+    if (field_type is int) or (field_type == "int"):
         try:
             return int(raw)
         except ValueError:
             raise exceptions.BadRequest(f"Query parameter {field_name!r} must 
be an integer")
-    if field_type is bool or field_type == "bool":
+    if (field_type is bool) or (field_type == "bool"):
         return raw.lower() in ("true", "1", "yes")
     return raw
 
@@ -402,11 +402,11 @@ def _unwrap_optional(hint: Any) -> tuple[Any, bool]:
     (typing.Union).
     """
     origin = get_origin(hint)
-    if origin is not types.UnionType and origin is not typing.Union:
+    if (origin is not types.UnionType) and (origin is not typing.Union):
         return hint, False
     args = get_args(hint)
     non_none = [a for a in args if a is not type(None)]
-    if len(non_none) == 1 and type(None) in args:
+    if (len(non_none) == 1) and (type(None) in args):
         return non_none[0], True
     return hint, False
 
diff --git a/atr/blueprints/post.py b/atr/blueprints/post.py
index ca9daeaf..0a8a7e55 100644
--- a/atr/blueprints/post.py
+++ b/atr/blueprints/post.py
@@ -63,7 +63,7 @@ def typed(func: Callable[..., Any]) -> web.RouteFunction[Any]:
     path, validated_params, literal_params, form_param, public = 
common.build_path(func)
     project_key_var = next((name for name, t in validated_params if t is 
safe.ProjectKey), None)
     check_access = (not public) and (project_key_var is not None)
-    form_safe_params = common.safe_params_for_type(form_param[1]) if 
form_param is not None else []
+    form_safe_params = common.safe_params_for_type(form_param[1]) if 
(form_param is not None) else []
 
     async def wrapper(*_args: Any, **kwargs: Any) -> Any:
         enhanced_session = await common.authenticate_public() if public else 
await common.authenticate()
diff --git a/atr/models/github.py b/atr/models/github.py
index f35b86a1..4a4d67eb 100644
--- a/atr/models/github.py
+++ b/atr/models/github.py
@@ -72,6 +72,6 @@ class TrustedPublisherPayload(schema.Subset):
         if value is None:
             return value
         now = int(time.time())
-        if value and now < value:
+        if value and (now < value):
             raise ValueError("Token not yet valid")
         return value
diff --git a/atr/paths.py b/atr/paths.py
index c3be80af..a9ddefa7 100644
--- a/atr/paths.py
+++ b/atr/paths.py
@@ -28,14 +28,14 @@ def base_path_for_revision(
     return pathlib.Path(get_unfinished_dir(), str(project_key), 
str(version_key), str(revision))
 
 
-def get_attestable_dir() -> pathlib.Path:
-    return pathlib.Path(config.get().ATTESTABLE_STORAGE_DIR)
-
-
 def get_archives_dir() -> pathlib.Path:
     return pathlib.Path(config.get().ARCHIVES_STORAGE_DIR)
 
 
+def get_attestable_dir() -> pathlib.Path:
+    return pathlib.Path(config.get().ATTESTABLE_STORAGE_DIR)
+
+
 def get_downloads_dir() -> pathlib.Path:
     return pathlib.Path(config.get().DOWNLOADS_STORAGE_DIR)
 
diff --git a/atr/ssh.py b/atr/ssh.py
index 533fc86f..0472d587 100644
--- a/atr/ssh.py
+++ b/atr/ssh.py
@@ -435,23 +435,6 @@ async def _step_05b_command_path_validate_write(path: str) 
-> tuple[safe.Project
     return project_key, version_key, None
 
 
-def _validate_path_common(path: str) -> None:
-    """Validate the common structural requirements for an rsync path 
argument."""
-    if not path.startswith("/"):
-        raise RsyncArgsError("The path argument should be an absolute path")
-    if not path.endswith("/"):
-        # Technically we could ignore this, because we rewrite the path anyway 
for writes
-        # But we should enforce good rsync usage practices
-        raise RsyncArgsError("The path argument should be a directory path, 
ending with a /")
-    if "//" in path:
-        raise RsyncArgsError("The path argument should not contain //")
-
-
-def _validate_tag_segment(segment: str) -> None:
-    if not all(c in _PATH_ALPHANUM for c in segment):
-        raise RsyncArgsError("The tag should contain only alphanumeric 
characters or hyphens")
-
-
 async def _step_06a_validate_read_permissions(
     ssh_uid: str,
     project: sql.Project,
@@ -684,6 +667,23 @@ async def _step_08_execute_rsync(process: 
asyncssh.SSHServerProcess, argv: list[
     return exit_status
 
 
+def _validate_path_common(path: str) -> None:
+    """Validate the common structural requirements for an rsync path 
argument."""
+    if not path.startswith("/"):
+        raise RsyncArgsError("The path argument should be an absolute path")
+    if not path.endswith("/"):
+        # Technically we could ignore this, because we rewrite the path anyway 
for writes
+        # But we should enforce good rsync usage practices
+        raise RsyncArgsError("The path argument should be a directory path, 
ending with a /")
+    if "//" in path:
+        raise RsyncArgsError("The path argument should not contain //")
+
+
+def _validate_tag_segment(segment: str) -> None:
+    if not all(c in _PATH_ALPHANUM for c in segment):
+        raise RsyncArgsError("The tag should contain only alphanumeric 
characters or hyphens")
+
+
 async def _wait_for_process_to_close(process: asyncssh.SSHServerProcess) -> 
None:
     """Ensure that SSH process cleanup tasks run to avoid unawaited 
coroutines."""
     try:
diff --git a/atr/tabulate.py b/atr/tabulate.py
index bc054fcd..132a0291 100644
--- a/atr/tabulate.py
+++ b/atr/tabulate.py
@@ -304,7 +304,7 @@ def _vote_identity(
         name = "-"
         asf_uid = from_email_lower.split("@")[0]
     else:
-        if "via" in from_raw and from_email_lower.replace("@", ".") in 
list_email:
+        if ("via" in from_raw) and (from_email_lower.replace("@", ".") in 
list_email):
             # Take the last CC, appended by ezmlm, and use that as the email. 
Otherwise, use their name
             name = from_raw[: from_raw.index("via") - 1]
             if cc:
diff --git a/atr/tasks/quarantine.py b/atr/tasks/quarantine.py
index 3b035bf7..72b47616 100644
--- a/atr/tasks/quarantine.py
+++ b/atr/tasks/quarantine.py
@@ -201,7 +201,7 @@ def _extract_archive_to_dir(
         try:
             os.rename(staging_dir, archive_dir)
         except OSError as err:
-            if isinstance(err, FileExistsError) or err.errno in {errno.EEXIST, 
errno.ENOTEMPTY}:
+            if isinstance(err, FileExistsError) or (err.errno in 
{errno.EEXIST, errno.ENOTEMPTY}):
                 shutil.rmtree(staging_dir, ignore_errors=True)
             else:
                 raise
diff --git a/atr/util.py b/atr/util.py
index eed4feae..558507a8 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -641,28 +641,6 @@ def key_ssh_fingerprint(ssh_key_string: str) -> str:
         raise SshFingerprintError(str(e)) from e
 
 
-def match_ignore_pattern(pattern: str | None, value: str | None) -> bool:
-
-    if pattern == "!":
-        # Special case, "!" matches None
-        return value is None
-    if (pattern is None) or (value is None):
-        return False
-    negate = False
-    raw_pattern = pattern
-    if raw_pattern.startswith("!"):
-        raw_pattern = raw_pattern[1:]
-        negate = True
-    try:
-        regex = validation.compile_ignore_pattern(raw_pattern)
-    except ValueError:
-        return False
-    matched = regex.search(value) is not None
-    if negate:
-        return not matched
-    return matched
-
-
 def key_ssh_fingerprint_core(ssh_key_string: str) -> str:
     # The format should be as in *.pub or authorized_keys files
     # I.e. TYPE DATA COMMENT
@@ -687,6 +665,28 @@ def key_ssh_fingerprint_core(ssh_key_string: str) -> str:
     raise ValueError("Invalid SSH key format")
 
 
+def match_ignore_pattern(pattern: str | None, value: str | None) -> bool:
+
+    if pattern == "!":
+        # Special case, "!" matches None
+        return value is None
+    if (pattern is None) or (value is None):
+        return False
+    negate = False
+    raw_pattern = pattern
+    if raw_pattern.startswith("!"):
+        raw_pattern = raw_pattern[1:]
+        negate = True
+    try:
+        regex = validation.compile_ignore_pattern(raw_pattern)
+    except ValueError:
+        return False
+    matched = regex.search(value) is not None
+    if negate:
+        return not matched
+    return matched
+
+
 async def number_of_release_files(release: sql.Release) -> int:
     """Return the number of files in a release."""
     if (path := paths.release_directory_revision(release)) is None:
diff --git a/atr/web.py b/atr/web.py
index 0498943d..3fa23077 100644
--- a/atr/web.py
+++ b/atr/web.py
@@ -182,7 +182,7 @@ class Committer:
             phase_value = sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT
         else:
             phase_value = phase
-        revision = db.NOT_SET if latest_revision_number == db.NOT_SET else 
str(latest_revision_number)
+        revision = db.NOT_SET if (latest_revision_number == db.NOT_SET) else 
str(latest_revision_number)
         release_key = sql.release_key(project_key, version_key)
         if data is None:
             async with db.session() as data:
diff --git a/pip-audit.requirements b/pip-audit.requirements
index ee0180c4..aef6ef78 100644
--- a/pip-audit.requirements
+++ b/pip-audit.requirements
@@ -30,11 +30,11 @@ arrow==1.4.0
     # via isoduration
 asfpy==0.58
     # via asfquart
-asfquart @ 
git+https://github.com/apache/infrastructure-asfquart.git@ae3b020002b63688c65715f4f45bd351d868da3d
+asfquart @ 
git+https://github.com/apache/infrastructure-asfquart.git@7ff9f50dcfca338bc935646323c1942275b85195
     # via tooling-trusted-releases
 asyncssh==2.22.0
     # via tooling-trusted-releases
-attrs==25.4.0
+attrs==26.1.0
     # via
     #   aiohttp
     #   jsonschema
@@ -150,7 +150,7 @@ hypercorn==0.18.0
     #   tooling-trusted-releases
 hyperframe==6.1.0
     # via h2
-hyperscan==0.8.1
+hyperscan==0.8.2
     # via tooling-trusted-releases
 identify==2.6.18
     # via pre-commit
@@ -271,7 +271,7 @@ python-dateutil==2.9.0.post0
     #   strictyaml
 python-decouple==3.8
     # via tooling-trusted-releases
-python-discovery==1.1.3
+python-discovery==1.2.0
     # via virtualenv
 python-gnupg==0.5.6
     # via tooling-trusted-releases
@@ -317,7 +317,7 @@ rpds-py==0.30.0
     # via
     #   jsonschema
     #   referencing
-ruff==0.15.6
+ruff==0.15.7
 semver==3.0.4
     # via tooling-trusted-releases
 six==1.17.0
diff --git a/tests/unit/test_blueprints.py b/tests/unit/test_blueprints.py
index 28e79279..d1bd31ac 100644
--- a/tests/unit/test_blueprints.py
+++ b/tests/unit/test_blueprints.py
@@ -70,25 +70,93 @@ async def 
test_all_routes_support_url_construction(monkeypatch):
         raise AssertionError("Routes incompatible with as_url:\n" + 
"\n".join(failures))
 
 
-def test_build_path_literal_segment():
-    async def route(_session: web.Committer, _page: Literal["dashboard"]) -> 
str:
+def test_build_api_path_body_param():
+    class RequestBody(schema.Strict):
+        value: int
+
+    async def route(_session: web.Committer, _page: Literal["submit"], _data: 
RequestBody) -> str:
         return ""
 
-    path, validated, literals, form_param, public = common.build_path(route)
-    assert path == "/dashboard"
-    assert literals == {"_page": "dashboard"}
-    assert validated == []
-    assert form_param is None
-    assert public is False
+    _, _, _, body, _, _, _ = common.build_api_path(route)
+    assert body is not None
+    assert body[0] == "_data"
+    assert body[1] is RequestBody
 
 
-def test_build_path_safe_type():
-    async def route(_session: web.Committer, _project_key: safe.ProjectKey) -> 
str:
+def test_build_api_path_literal_and_safe():
+    async def route(
+        _session: web.Committer,
+        _page: Literal["project"],
+        _project_key: safe.ProjectKey,
+    ) -> str:
         return ""
 
-    path, validated, _, _, _ = common.build_path(route)
-    assert path == "/<_project_key>"
+    path, validated, literals, body, form_param, query, optional = 
common.build_api_path(route)
+    assert path == "/project/<_project_key>"
     assert validated == [("_project_key", safe.ProjectKey)]
+    assert literals == {"_page": "project"}
+    assert body is None
+    assert form_param is None
+    assert query is None
+    assert optional == []
+
+
+def test_build_api_path_optional_param():
+    async def route(
+        _session: web.Committer,
+        _page: Literal["items"],
+        _category: str | None = None,
+    ) -> str:
+        return ""
+
+    path, _, _, _, _, _, optional = common.build_api_path(route)
+    assert path == "/items/<_category>"
+    assert optional == ["_category"]
+
+
+def test_build_api_path_query_param():
+    @dataclasses.dataclass
+    class Filters:
+        page: int = 1
+        search: str = ""
+
+    async def route(_session: web.Committer, _page: Literal["list"], _filters: 
Filters) -> str:
+        return ""
+
+    _, _, _, _, _, query, _ = common.build_api_path(route)
+    assert query is not None
+    assert query[0] == "_filters"
+    assert query[1] is Filters
+
+
+def test_build_api_path_rejects_duplicate_body():
+    class BodyA(schema.Lax):
+        x: int
+
+    class BodyB(schema.Lax):
+        y: int
+
+    async def route(_session: web.Committer, _a: BodyA, _b: BodyB) -> str:
+        return ""
+
+    with pytest.raises(TypeError, match="only one body type is allowed"):
+        common.build_api_path(route)
+
+
+def test_build_api_path_rejects_duplicate_query():
+    @dataclasses.dataclass
+    class QueryA:
+        x: int = 0
+
+    @dataclasses.dataclass
+    class QueryB:
+        y: int = 0
+
+    async def route(_session: web.Committer, _a: QueryA, _b: QueryB) -> str:
+        return ""
+
+    with pytest.raises(TypeError, match="only one query type is allowed"):
+        common.build_api_path(route)
 
 
 def test_build_path_combined_literal_and_safe():
@@ -107,12 +175,18 @@ def test_build_path_combined_literal_and_safe():
     assert literals == {"_page": "project", "_sub": "version"}
 
 
-def test_build_path_public_session():
-    async def route(_session: web.Public, _page: Literal["home"]) -> str:
+def test_build_path_form_param():
+    class TestForm(form.Form):
+        name: str = ""
+
+    async def route(_session: web.Committer, _page: Literal["submit"], _data: 
TestForm) -> str:
         return ""
 
-    _, _, _, _, public = common.build_path(route)
-    assert public is True
+    path, _, _, form_param, _ = common.build_path(route)
+    assert path == "/submit"
+    assert form_param is not None
+    assert form_param[0] == "_data"
+    assert form_param[1] is TestForm
 
 
 def test_build_path_int_converter():
@@ -123,18 +197,32 @@ def test_build_path_int_converter():
     assert path == "/item/<int:_item_id>"
 
 
-def test_build_path_form_param():
-    class TestForm(form.Form):
-        name: str = ""
+def test_build_path_literal_segment():
+    async def route(_session: web.Committer, _page: Literal["dashboard"]) -> 
str:
+        return ""
 
-    async def route(_session: web.Committer, _page: Literal["submit"], _data: 
TestForm) -> str:
+    path, validated, literals, form_param, public = common.build_path(route)
+    assert path == "/dashboard"
+    assert literals == {"_page": "dashboard"}
+    assert validated == []
+    assert form_param is None
+    assert public is False
+
+
+def test_build_path_public_session():
+    async def route(_session: web.Public, _page: Literal["home"]) -> str:
         return ""
 
-    path, _, _, form_param, _ = common.build_path(route)
-    assert path == "/submit"
-    assert form_param is not None
-    assert form_param[0] == "_data"
-    assert form_param[1] is TestForm
+    _, _, _, _, public = common.build_path(route)
+    assert public is True
+
+
+def test_build_path_rejects_bare_str():
+    async def route(_session: web.Committer, _name: str) -> str:
+        return ""
+
+    with pytest.raises(TypeError, match="unguarded str"):
+        common.build_path(route)
 
 
 def test_build_path_rejects_duplicate_form():
@@ -167,101 +255,34 @@ def test_build_path_rejects_unannotated_param():
         common.build_path(route)
 
 
-def test_build_path_rejects_bare_str():
-    async def route(_session: web.Committer, _name: str) -> str:
-        return ""
-
-    with pytest.raises(TypeError, match="unguarded str"):
-        common.build_path(route)
-
-
-def test_build_api_path_literal_and_safe():
-    async def route(
-        _session: web.Committer,
-        _page: Literal["project"],
-        _project_key: safe.ProjectKey,
-    ) -> str:
+def test_build_path_safe_type():
+    async def route(_session: web.Committer, _project_key: safe.ProjectKey) -> 
str:
         return ""
 
-    path, validated, literals, body, form_param, query, optional = 
common.build_api_path(route)
-    assert path == "/project/<_project_key>"
+    path, validated, _, _, _ = common.build_path(route)
+    assert path == "/<_project_key>"
     assert validated == [("_project_key", safe.ProjectKey)]
-    assert literals == {"_page": "project"}
-    assert body is None
-    assert form_param is None
-    assert query is None
-    assert optional == []
-
-
-def test_build_api_path_body_param():
-    class RequestBody(schema.Strict):
-        value: int
-
-    async def route(_session: web.Committer, _page: Literal["submit"], _data: 
RequestBody) -> str:
-        return ""
-
-    _, _, _, body, _, _, _ = common.build_api_path(route)
-    assert body is not None
-    assert body[0] == "_data"
-    assert body[1] is RequestBody
-
-
-def test_build_api_path_query_param():
-    @dataclasses.dataclass
-    class Filters:
-        page: int = 1
-        search: str = ""
-
-    async def route(_session: web.Committer, _page: Literal["list"], _filters: 
Filters) -> str:
-        return ""
-
-    _, _, _, _, _, query, _ = common.build_api_path(route)
-    assert query is not None
-    assert query[0] == "_filters"
-    assert query[1] is Filters
-
-
-def test_build_api_path_optional_param():
-    async def route(
-        _session: web.Committer,
-        _page: Literal["items"],
-        _category: str | None = None,
-    ) -> str:
-        return ""
 
-    path, _, _, _, _, _, optional = common.build_api_path(route)
-    assert path == "/items/<_category>"
-    assert optional == ["_category"]
-
-
-def test_build_api_path_rejects_duplicate_body():
-    class BodyA(schema.Lax):
-        x: int
-
-    class BodyB(schema.Lax):
-        y: int
-
-    async def route(_session: web.Committer, _a: BodyA, _b: BodyB) -> str:
-        return ""
-
-    with pytest.raises(TypeError, match="only one body type is allowed"):
-        common.build_api_path(route)
 
+def test_safe_params_for_type_empty_for_plain_model():
+    class Body(schema.Strict):
+        name: str
+        count: int
 
-def test_build_api_path_rejects_duplicate_query():
-    @dataclasses.dataclass
-    class QueryA:
-        x: int = 0
+    result = common.safe_params_for_type(Body)
+    assert result == []
 
-    @dataclasses.dataclass
-    class QueryB:
-        y: int = 0
 
-    async def route(_session: web.Committer, _a: QueryA, _b: QueryB) -> str:
-        return ""
+def test_safe_params_for_type_finds_safe_fields():
+    class Body(schema.Strict):
+        project_key: safe.ProjectKey
+        version_key: safe.VersionKey
+        description: str
 
-    with pytest.raises(TypeError, match="only one query type is allowed"):
-        common.build_api_path(route)
+    result = common.safe_params_for_type(Body)
+    assert ("project_key", safe.ProjectKey) in result
+    assert ("version_key", safe.VersionKey) in result
+    assert len(result) == 2
 
 
 def test_setup_wrapper_sets_metadata():
@@ -279,24 +300,3 @@ def test_setup_wrapper_sets_metadata():
     assert wrapper.__name__ == "index"
     assert wrapper.__doc__ == "Doc string."
     assert wrapper.__annotations__["endpoint"] == 
"get_blueprint.atr_get_dashboard_index"
-
-
-def test_safe_params_for_type_finds_safe_fields():
-    class Body(schema.Strict):
-        project_key: safe.ProjectKey
-        version_key: safe.VersionKey
-        description: str
-
-    result = common.safe_params_for_type(Body)
-    assert ("project_key", safe.ProjectKey) in result
-    assert ("version_key", safe.VersionKey) in result
-    assert len(result) == 2
-
-
-def test_safe_params_for_type_empty_for_plain_model():
-    class Body(schema.Strict):
-        name: str
-        count: int
-
-    result = common.safe_params_for_type(Body)
-    assert result == []
diff --git a/tests/unit/test_detection.py b/tests/unit/test_detection.py
index 127bbac4..d089ecc5 100644
--- a/tests/unit/test_detection.py
+++ b/tests/unit/test_detection.py
@@ -26,6 +26,28 @@ import atr.models.attestable as models
 type TarArchiveEntry = tuple[str, str, bytes | str]
 
 
+def test_check_archive_safety_accepts_dotenv_anywhere_in_tar_and_zip(tmp_path):
+    tar_path = tmp_path / "safe-dotenv.tar.gz"
+    _write_tar_gz(
+        tar_path,
+        [
+            _tar_regular_file(".env", b"ATR_STATUS=ALPHA\n"),
+            _tar_regular_file("config/.env", b"SECRET=value\n"),
+        ],
+    )
+    zip_path = tmp_path / "safe-dotenv.zip"
+    _write_zip(
+        zip_path,
+        [
+            (".env", b"ATR_STATUS=ALPHA\n"),
+            ("config/.env", b"SECRET=value\n"),
+        ],
+    )
+
+    assert detection.check_archive_safety(str(tar_path)) == []
+    assert detection.check_archive_safety(str(zip_path)) == []
+
+
 def test_check_archive_safety_accepts_safe_tar_gz(tmp_path):
     archive_path = tmp_path / "safe.tar.gz"
     _write_tar_gz(
@@ -52,28 +74,6 @@ def test_check_archive_safety_accepts_safe_zip(tmp_path):
     assert detection.check_archive_safety(str(archive_path)) == []
 
 
-def test_check_archive_safety_accepts_dotenv_anywhere_in_tar_and_zip(tmp_path):
-    tar_path = tmp_path / "safe-dotenv.tar.gz"
-    _write_tar_gz(
-        tar_path,
-        [
-            _tar_regular_file(".env", b"ATR_STATUS=ALPHA\n"),
-            _tar_regular_file("config/.env", b"SECRET=value\n"),
-        ],
-    )
-    zip_path = tmp_path / "safe-dotenv.zip"
-    _write_zip(
-        zip_path,
-        [
-            (".env", b"ATR_STATUS=ALPHA\n"),
-            ("config/.env", b"SECRET=value\n"),
-        ],
-    )
-
-    assert detection.check_archive_safety(str(tar_path)) == []
-    assert detection.check_archive_safety(str(zip_path)) == []
-
-
 def test_check_archive_safety_rejects_absolute_paths_in_tar_and_zip(tmp_path):
     tar_path = tmp_path / "unsafe-absolute.tar.gz"
     _write_tar_gz(
diff --git a/tests/unit/test_safe_types.py b/tests/unit/test_safe_types.py
index 593d8250..1edb0a28 100644
--- a/tests/unit/test_safe_types.py
+++ b/tests/unit/test_safe_types.py
@@ -44,6 +44,12 @@ def test_safe_types_reject_invalid_characters(cls: 
type[safe.Alphanumeric], bad:
         cls(bad)
 
 
[email protected]("cls", [safe.Alphanumeric, safe.ProjectKey])
+def test_safe_alpha_types_reject_valid_version(cls: type[safe.Alphanumeric]):
+    with pytest.raises(ValueError):
+        cls("0.1+def")
+
+
 @pytest.mark.parametrize("cls", [safe.Alphanumeric, safe.ProjectKey, 
safe.VersionKey, safe.ReleaseKey])
 def test_safe_types_accept_valid_alpha(cls: type[safe.Alphanumeric]):
     value = cls("abcdef")
@@ -54,9 +60,3 @@ def test_safe_types_accept_valid_alpha(cls: 
type[safe.Alphanumeric]):
 def test_safe_version_types_accept_valid_version(cls: type[safe.Alphanumeric]):
     value = cls("0.1+def")
     assert str(value) == "0.1+def"
-
-
[email protected]("cls", [safe.Alphanumeric, safe.ProjectKey])
-def test_safe_alpha_types_reject_valid_version(cls: type[safe.Alphanumeric]):
-    with pytest.raises(ValueError):
-        cls("0.1+def")
diff --git a/uv.lock b/uv.lock
index 7a28b2e0..1f207ab9 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,7 +3,7 @@ revision = 3
 requires-python = "==3.13.*"
 
 [options]
-exclude-newer = "2026-03-17T17:33:15Z"
+exclude-newer = "2026-03-20T16:31:27Z"
 
 [[package]]
 name = "aiofiles"
@@ -175,7 +175,7 @@ wheels = [
 [[package]]
 name = "asfquart"
 version = "0.1.13"
-source = { git = 
"https://github.com/apache/infrastructure-asfquart.git?rev=main#ae3b020002b63688c65715f4f45bd351d868da3d";
 }
+source = { git = 
"https://github.com/apache/infrastructure-asfquart.git?rev=main#7ff9f50dcfca338bc935646323c1942275b85195";
 }
 dependencies = [
     { name = "aiohttp" },
     { name = "asfpy" },
@@ -201,11 +201,11 @@ wheels = [
 
 [[package]]
 name = "attrs"
-version = "25.4.0"
+version = "26.1.0"
 source = { registry = "https://pypi.org/simple"; }
-sdist = { url = 
"https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz";,
 hash = 
"sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size 
= 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz";,
 hash = 
"sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size 
= 952055, upload-time = "2026-03-19T14:22:25.026Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl";,
 hash = 
"sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size 
= 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+    { url = 
"https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl";,
 hash = 
"sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size 
= 67548, upload-time = "2026-03-19T14:22:23.645Z" },
 ]
 
 [[package]]
@@ -779,17 +779,17 @@ wheels = [
 
 [[package]]
 name = "hyperscan"
-version = "0.8.1"
+version = "0.8.2"
 source = { registry = "https://pypi.org/simple"; }
-sdist = { url = 
"https://files.pythonhosted.org/packages/cc/56/6359bfa86a3bfeeecbeeb1ffcfc9484058c8a8ae153c8bda2181c3511e6b/hyperscan-0.8.1.tar.gz";,
 hash = 
"sha256:d50bf70b0110817a308bfb1855055dc4d649934857b958498a1791164f512779", size 
= 125303, upload-time = "2026-02-11T19:19:46.323Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/c3/26/21daad311299a416059cf1919c51410573180cf7133b42927693f19c0af7/hyperscan-0.8.2.tar.gz";,
 hash = 
"sha256:1724e87e8f77f033a4592dc2cda7aecd10c91dfc718b55fa5379d0c95cff28e8", size 
= 125600, upload-time = "2026-03-19T01:47:34.538Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/17/b2/6060a2a84a2dae7024d03719ecf6b0438b6f40aeba11a34ede6ffdea7b91/hyperscan-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl";,
 hash = 
"sha256:ee39b88a85fbd0b262e2a458985f878f6cf726e41099c601e0c2ce681aa16d98", size 
= 2044166, upload-time = "2026-02-11T19:19:09.761Z" },
-    { url = 
"https://files.pythonhosted.org/packages/ad/44/6a727f676c0cf86efed79320dfe968d53d66f8f2df4f9ebab33cabd648c9/hyperscan-0.8.1-cp313-cp313-macosx_11_0_arm64.whl";,
 hash = 
"sha256:6ce5d67e90ad18800f68a7ef52fdba60223ff5ebfa19b945d6abbc0a6163e69f", size 
= 2033045, upload-time = "2026-02-11T19:19:11.156Z" },
-    { url = 
"https://files.pythonhosted.org/packages/a0/32/6edc476f9623ef7f87dc851e28803ad0b765202f129f399223a7b917fb32/hyperscan-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl";,
 hash = 
"sha256:8d504b8fda00678d86cd1ea63ca15ef19e553da411636eb36acc1c85d1d0ee2c", size 
= 2763694, upload-time = "2026-02-11T19:19:12.522Z" },
-    { url = 
"https://files.pythonhosted.org/packages/d6/83/e05ec0da2f856925dae6c978fc67c354d9a48712626cc455c891995b236f/hyperscan-0.8.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:9f25ec48d0a8d3d97ebc758fdfa601c89db26039004c7f1ce2823249adbc7960", size 
= 2567752, upload-time = "2026-02-11T19:19:13.932Z" },
-    { url = 
"https://files.pythonhosted.org/packages/4e/b0/44679375c66a7ee30c05e31f92cbeeade4bd9efe225b0e4080657a2258de/hyperscan-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:4952a74ab75196ce937d3d60d37fe7157e6b570b87726366f6ec7a22f204519d", size 
= 2389687, upload-time = "2026-02-11T19:19:15.485Z" },
-    { url = 
"https://files.pythonhosted.org/packages/af/32/e056369242414849e8ea4ea6efd03fdccf05b953623baca77b5ac6a33640/hyperscan-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:3f22cfcba25ff91ecaa655eb5e666760e2f5fde1af6c91548b71a7336cb0531e", size 
= 2429033, upload-time = "2026-02-11T19:19:16.843Z" },
-    { url = 
"https://files.pythonhosted.org/packages/0c/ac/2e53b22605671a872fb2a8ef8ff40a051ced0caae71d91a690af4f08fffe/hyperscan-0.8.1-cp313-cp313-win_amd64.whl";,
 hash = 
"sha256:773fc1373a6a12b09e3b1580dcb238dc5e2ebc17482284a436958cbb432e439f", size 
= 1956074, upload-time = "2026-02-11T19:19:18.571Z" },
+    { url = 
"https://files.pythonhosted.org/packages/fc/fd/34ed5d1ddb1b0ad384a05b5afdb1f302c145cb4bb885a1cd91266be04740/hyperscan-0.8.2-cp313-cp313-macosx_10_13_x86_64.whl";,
 hash = 
"sha256:4fee39d8af5738e51dd6aa3684ffcb1c782dfa907a7a64f50c599635e80606dc", size 
= 2044020, upload-time = "2026-03-19T01:46:56.576Z" },
+    { url = 
"https://files.pythonhosted.org/packages/e6/2b/a222d1cce1d203ef9c14ab48d6b5d8c9e3c457a7ebf29ed8dcd9b5ff9193/hyperscan-0.8.2-cp313-cp313-macosx_11_0_arm64.whl";,
 hash = 
"sha256:7bdac73df001759538f9beee957ac2224739b5ac49814f96a6c3cd2a1fcdafa0", size 
= 2032948, upload-time = "2026-03-19T01:46:58.688Z" },
+    { url = 
"https://files.pythonhosted.org/packages/74/d7/44b8879c6e6e5c32f3d47f6be425778bd4124a5f19d0d30610f60a61f817/hyperscan-0.8.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl";,
 hash = 
"sha256:177692a7688e64e1c77f0af5f23eaad937c452798cd15c0db86bf98b5dce4671", size 
= 2763696, upload-time = "2026-03-19T01:47:00.159Z" },
+    { url = 
"https://files.pythonhosted.org/packages/48/0f/d0014ef543ef7327c437337905acbba271632698bd755673126d698bb1fe/hyperscan-0.8.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:7ec49927002a38ac767d0f18e17135602e493bf2f720548bf7d43a3af2f810a0", size 
= 2567752, upload-time = "2026-03-19T01:47:01.97Z" },
+    { url = 
"https://files.pythonhosted.org/packages/a7/25/e25ce2c7b76d758e3ca8013e1df3c7388240e9f72e07f003ce55f0fef628/hyperscan-0.8.2-cp313-cp313-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:1055fac1eec046bfc67254d4ea900852597b2eca8e7219e3e558fb869c48100e", size 
= 2389688, upload-time = "2026-03-19T01:47:03.482Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d1/bd/b0afe3df17a843a9df3cd60e6a63b31b6c3d5a672f5641eb64eeb91a1707/hyperscan-0.8.2-cp313-cp313-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:d94495f8be1c0efe9e24ca3f10796c23921f8556a53b20d5619d4e96861d2f59", size 
= 2429031, upload-time = "2026-03-19T01:47:05.088Z" },
+    { url = 
"https://files.pythonhosted.org/packages/e8/62/9e62e22214b47fbd42c58397691d119cb73c0e60ca6a932cf597aaf65f30/hyperscan-0.8.2-cp313-cp313-win_amd64.whl";,
 hash = 
"sha256:7d5a6ac08dab6c9879c87221858371d63545c08920e09bffa258a555843f6ef3", size 
= 1956255, upload-time = "2026-03-19T01:47:06.645Z" },
 ]
 
 [[package]]
@@ -1493,15 +1493,15 @@ wheels = [
 
 [[package]]
 name = "python-discovery"
-version = "1.1.3"
+version = "1.2.0"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "filelock" },
     { name = "platformdirs" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/d7/7e/9f3b0dd3a074a6c3e1e79f35e465b1f2ee4b262d619de00cfce523cc9b24/python_discovery-1.1.3.tar.gz";,
 hash = 
"sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5", size 
= 56945, upload-time = "2026-03-10T15:08:15.038Z" }
+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" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/e7/80/73211fc5bfbfc562369b4aa61dc1e4bf07dc7b34df7b317e4539316b809c/python_discovery-1.1.3-py3-none-any.whl";,
 hash = 
"sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e", size 
= 31485, upload-time = "2026-03-10T15:08:13.06Z" },
+    { 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" },
 ]
 
 [[package]]
@@ -1774,27 +1774,27 @@ wheels = [
 
 [[package]]
 name = "ruff"
-version = "0.15.6"
-source = { registry = "https://pypi.org/simple"; }
-sdist = { url = 
"https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz";,
 hash = 
"sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size 
= 4597916, upload-time = "2026-03-12T23:05:47.51Z" }
-wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl";,
 hash = 
"sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size 
= 10508953, upload-time = "2026-03-12T23:05:17.246Z" },
-    { url = 
"https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size 
= 10942257, upload-time = "2026-03-12T23:05:23.076Z" },
-    { url = 
"https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl";,
 hash = 
"sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size 
= 10322683, upload-time = "2026-03-12T23:05:33.738Z" },
-    { url = 
"https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl";,
 hash = 
"sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size 
= 10660986, upload-time = "2026-03-12T23:05:53.245Z" },
-    { url = 
"https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl";,
 hash = 
"sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size 
= 10332177, upload-time = "2026-03-12T23:05:56.145Z" },
-    { url = 
"https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl";,
 hash = 
"sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size 
= 11170783, upload-time = "2026-03-12T23:06:01.742Z" },
-    { url = 
"https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl";,
 hash = 
"sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size 
= 12044201, upload-time = "2026-03-12T23:05:28.697Z" },
-    { url = 
"https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl";,
 hash = 
"sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size 
= 11421561, upload-time = "2026-03-12T23:05:31.236Z" },
-    { url = 
"https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size 
= 11310928, upload-time = "2026-03-12T23:05:45.288Z" },
-    { url = 
"https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl";,
 hash = 
"sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size 
= 11235186, upload-time = "2026-03-12T23:05:50.677Z" },
-    { url = 
"https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size 
= 10635231, upload-time = "2026-03-12T23:05:37.044Z" },
-    { url = 
"https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl";,
 hash = 
"sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size 
= 10340357, upload-time = "2026-03-12T23:06:04.748Z" },
-    { url = 
"https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl";,
 hash = 
"sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size 
= 10860583, upload-time = "2026-03-12T23:05:58.978Z" },
-    { url = 
"https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size 
= 11410976, upload-time = "2026-03-12T23:05:39.955Z" },
-    { url = 
"https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl";,
 hash = 
"sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size 
= 10616872, upload-time = "2026-03-12T23:05:42.451Z" },
-    { url = 
"https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl";,
 hash = 
"sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size 
= 11787271, upload-time = "2026-03-12T23:05:20.168Z" },
-    { url = 
"https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl";,
 hash = 
"sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size 
= 11060497, upload-time = "2026-03-12T23:05:25.968Z" },
+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" },
 ]
 
 [[package]]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to