Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lambda_handlers/rale_authorizer/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def handler(event: dict[str, Any], context: Any) -> dict[str, Any]: # noqa: ARG
allowed = service.has_package_grant(project_id=project_id, quilt_uri=quilt_uri)
except DataZoneError:
return _response(503, {"error": "authorization service unavailable"})
except ClientError, BotoCoreError:
except (ClientError, BotoCoreError):
return _response(503, {"error": "authorization service unavailable"})

if not allowed:
Expand All @@ -248,7 +248,7 @@ def handler(event: dict[str, Any], context: Any) -> dict[str, Any]: # noqa: ARG
if jwt_secret_version:
secret_kwargs["VersionId"] = jwt_secret_version
jwt_secret = secrets.get_secret_value(**secret_kwargs)["SecretString"]
except ClientError, BotoCoreError, KeyError:
except (ClientError, BotoCoreError, KeyError):
return _response(503, {"error": "failed to load jwt secret"})

token_ttl = int(os.environ.get("TOKEN_TTL", "3600"))
Expand Down
6 changes: 3 additions & 3 deletions lambda_handlers/rale_router/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def _proxy_get_or_head(
)
except s3_client.exceptions.NoSuchKey:
return _response(404, {"error": "object not found"})
except ClientError, BotoCoreError:
except (ClientError, BotoCoreError):
return _response(502, {"error": "failed to fetch object from S3"})


Expand Down Expand Up @@ -161,14 +161,14 @@ def handler(event: dict[str, Any], context: Any) -> dict[str, Any]: # noqa: ARG
if jwt_secret_version:
secret_kwargs["VersionId"] = jwt_secret_version
jwt_secret = secrets.get_secret_value(**secret_kwargs)["SecretString"]
except ClientError, BotoCoreError, KeyError:
except (ClientError, BotoCoreError, KeyError):
return _response(503, {"error": "failed to load jwt secret"})

try:
claims = validate_taj_token(taj, jwt_secret)
except TokenExpiredError:
return _response(401, {"error": "expired TAJ"})
except TokenInvalidError, TokenValidationError:
except (TokenInvalidError, TokenValidationError):
return _response(401, {"error": "invalid TAJ"})

# For un-pinned USLs the hash comes from the TAJ; for pinned USLs validate
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ build-backend = "uv_build"
rale = "raja.cli:main"

[tool.ruff]
target-version = "py314"
# target-version is inferred from project.requires-python, so the formatter and
# linter never emit syntax that is newer than the interpreters we support.
line-length = 100
extend-exclude = ["infra/cdk.out", "infra/cdk.out*"]

Expand Down
6 changes: 3 additions & 3 deletions src/raja/datazone/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ def _get_asset_external_identifier(self, asset_id: str) -> str:
domainIdentifier=self._config.domain_id,
identifier=asset_id,
)
except ClientError, BotoCoreError:
except (ClientError, BotoCoreError):
return ""
if not isinstance(response, dict):
return ""
Expand Down Expand Up @@ -263,7 +263,7 @@ def _get_iam_arn_for_user_id(self, user_id: str) -> str | None:
)
arn: str | None = resp.get("details", {}).get("iam", {}).get("arn")
return arn
except ClientError, BotoCoreError:
except (ClientError, BotoCoreError):
return None

def _get_user_id_for_principal(self, principal: str) -> str | None:
Expand All @@ -276,7 +276,7 @@ def _get_user_id_for_principal(self, principal: str) -> str | None:
)
user_id: str | None = resp.get("id")
return user_id
except ClientError, BotoCoreError:
except (ClientError, BotoCoreError):
return None

def _resolve_membership_user_identifier(self, user_identifier: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion src/raja/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def _normalize_scopes(scopes: Iterable[Scope | str]) -> set[str]:
normalized.add(format_scope(scope.resource_type, scope.resource_id, scope.action))
else:
normalized.add(format_scope(**parse_scope(scope).model_dump()))
except ScopeParseError, ScopeValidationError:
except (ScopeParseError, ScopeValidationError):
# Re-raise our custom exceptions
raise
except Exception as exc:
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/test_python_floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Guard the minimum Python version declared in pyproject.toml.

The published package and the Lambda bundles must parse on the oldest
interpreter ``requires-python`` allows. The CI matrix cannot be relied on for
this: ``uv sync`` resolves the interpreter from ``.python-version``, so every
matrix job runs the same version regardless of ``actions/setup-python``.
"""

from __future__ import annotations

import ast
import re
import tomllib
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]
SOURCE_ROOTS = (REPO_ROOT / "src", REPO_ROOT / "lambda_handlers")


def minimum_python() -> tuple[int, int]:
"""Return the lowest (major, minor) accepted by ``requires-python``."""
pyproject = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text())
requires_python = pyproject["project"]["requires-python"]
match = re.search(r">=\s*(\d+)\.(\d+)", requires_python)
if not match:
pytest.fail(f"cannot read a minimum version from requires-python: {requires_python}")
return int(match.group(1)), int(match.group(2))


def source_files() -> list[Path]:
return sorted(path for root in SOURCE_ROOTS for path in root.rglob("*.py"))


def test_sources_parse_on_minimum_python() -> None:
floor = minimum_python()
failures: list[str] = []

for path in source_files():
try:
ast.parse(path.read_text(), filename=str(path), feature_version=floor)
except SyntaxError as exc:
relative = path.relative_to(REPO_ROOT)
failures.append(f"{relative}:{exc.lineno}: {exc.msg}")

assert not failures, "syntax not supported on Python {}.{}:\n{}".format(
floor[0], floor[1], "\n".join(failures)
)


def test_feature_version_rejects_newer_syntax() -> None:
"""Fail loudly if ``feature_version`` stops gating syntax we rely on it for."""
floor = minimum_python()
if floor >= (3, 14):
pytest.skip("minimum Python already allows unparenthesized except expressions")

source = "try:\n pass\nexcept ValueError, TypeError:\n pass\n"
with pytest.raises(SyntaxError):
ast.parse(source, feature_version=floor)