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]