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
76 changes: 57 additions & 19 deletions .github/workflows/quality_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ name: Quality check - check code

# PROCESS
#
# 1. Install all dependencies and spin off containers for all supported Python versions
# 2. Run code formatters and linters (various checks) for code standard
# 3. Run static typing checker for potential bugs
# 4. Run entire test suite for regressions except end-to-end (unit, functional, performance)
# 5. Run static analysis (in addition to CodeQL) for common insecure code practices
# 6. Run complexity baseline to avoid error-prone bugs and keep maintenance lower
# 7. Collect and report on test coverage
# 1. Run formatting, linting, security and complexity checks once
# 2. Run both type checkers and the test suite on every supported Python version
# 3. Validate optional dependencies with Nox on every supported Python version
# 4. Collect and report on test coverage

# USAGE
#
Expand All @@ -25,6 +22,8 @@ on:
- "mypy.ini"
- "Makefile"
- "noxfile.py"
- "ruff.toml"
- "bandit.baseline"
- ".pre-commit-config.yaml"
- ".github/workflows/quality_check.yml"
- ".github/workflows/release-v3.yml"
Expand All @@ -43,6 +42,8 @@ on:
- "mypy.ini"
- "Makefile"
- "noxfile.py"
- "ruff.toml"
- "bandit.baseline"
- ".pre-commit-config.yaml"
- ".github/workflows/quality_check.yml"
- ".github/workflows/release-v3.yml"
Expand All @@ -55,7 +56,39 @@ on:
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
static_checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
- name: Set up uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "0.12.19"
enable-cache: true
cache-suffix: static-checks
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Install dependencies
run: make dev-quality-code
- name: Check formatting
run: make format-check
- name: Lint
run: make lint-check
- name: Security baseline
run: make security-baseline
- name: Complexity baseline
run: make complexity-baseline

quality_check:
runs-on: ubuntu-latest
strategy:
Expand All @@ -69,21 +102,30 @@ jobs:
contents: read # checkout code only
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Set up uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "0.12.19"
enable-cache: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache-suffix: quality-${{ matrix.python-version }}
cache-dependency-glob: |
pyproject.toml
uv.lock
noxfile.py
- name: Install dependencies
run: make dev-quality-code
- name: Checking and enforcing format
run: make format-check
- name: Formatting and Linting
run: make lint
- name: Cache mypy analysis
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .mypy_cache
key: mypy-${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'mypy.ini') }}-${{ github.sha }}
restore-keys: |
mypy-${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'mypy.ini') }}-
- name: Static type checking (mypy)
run: make mypy
- name: Static type checking (ty)
Expand All @@ -92,10 +134,6 @@ jobs:
run: make test
- name: Test dependencies with Nox
run: make test-dependencies
- name: Security baseline
run: make security-baseline
- name: Complexity baseline
run: make complexity-baseline
- name: Upload coverage to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # 7.1.1
with:
Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then run
This creates a `.venv` with the locked development dependencies and installs the Git hooks.
Run tools with `uv run --locked`, or activate `.venv` when working in your editor.

CI runs formatting, linting, security and complexity checks once. Tests, dependency
checks with Nox, and both type checkers run on every supported Python version.
Run `make format-check lint-check` to check formatting and linting without modifying
files. `make lint` still formats files before linting. CI uses `make dev-quality-code`
to install the same dependencies as `make dev`, without installing Git hooks.

`make test-dependencies` recreates the Nox environments so packages left over from
earlier runs cannot hide missing dependencies. Tests marked `requires_validation`
run in dedicated sessions with the Validation extra, keeping the SDK and Parser
sessions independent of it. The full test suite still runs these tests.

To send us a pull request, please follow these steps:

1. Create a new branch to focus on the specific change you are contributing e.g. `improv/logger-debug-sampling`
Expand Down
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
.PHONY: target dev format lint test coverage-html pr build build-docs build-docs-website
.PHONY: target dev dev-quality-code format format-check lint lint-check test coverage-html pr build build-docs build-docs-website
.PHONY: docs-local security-baseline complexity-baseline release-prod release-test release

target:
@$(MAKE) pr

dev dev-quality-code:
uv sync --locked --extra all --extra redis --extra datamasking --extra valkey
dev: dev-quality-code
uv run --locked pre-commit install

dev-quality-code:
uv sync --locked --extra all --extra redis --extra datamasking --extra valkey

format-check:
uv run --locked ruff format aws_lambda_powertools tests examples --check

format:
uv run --locked ruff format aws_lambda_powertools tests examples

lint: format
$(MAKE) lint-check

lint-check:
uv run --locked ruff check aws_lambda_powertools tests examples

lint-docs:
Expand All @@ -28,7 +33,7 @@ test:
uv run --locked pytest --cache-clear tests/performance

test-dependencies:
uv run --locked nox --error-on-external-run --reuse-venv=yes --non-interactive
uv run --locked nox --error-on-external-run --reuse-venv=no --non-interactive

test-pydanticv2:
uv run --locked pytest -m "not perf" --ignore tests/e2e
Expand Down
51 changes: 46 additions & 5 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Run nox tests
#
# usage:
# uv run --locked nox --error-on-external-run --reuse-venv=yes --non-interactive
# uv run --locked nox --error-on-external-run --reuse-venv=no --non-interactive
#
# If you want to target a specific Python version, add -p parameter
from __future__ import annotations
Expand All @@ -14,7 +14,12 @@
PREFIX_TESTS_UNIT = "tests/unit"


def build_and_run_test(session: nox.Session, folders: list, extras: str = "") -> None:
def build_and_run_test(
session: nox.Session,
folders: list,
extras: str = "",
pytest_args: tuple[str, ...] = (),
) -> None:
"""
This function is responsible for setting up the testing environment and running the test suite for specific feature.

Expand All @@ -32,20 +37,29 @@ def build_and_run_test(session: nox.Session, folders: list, extras: str = "") ->
extras: Optional[str]
A string representing additional dependencies that should be installed for the test environment.
If not provided, the function will install the project with basic dependencies
pytest_args: tuple[str, ...]
Additional pytest arguments for selecting tests with matching dependencies
"""

# Required install to execute any test
# Shared fixtures use JSON Schema validation; previously Poetry installed it transitively.
session.install("pytest", "pytest-mock", "pytest_socket", "pytest-asyncio", "fastjsonschema")
session.install("pytest", "pytest-mock", "pytest_socket", "pytest-asyncio")

# Powertools project folder is in the root
if extras:
session.install(f"./[{extras}]")
else:
session.install("./")
session.run(
"python",
"-I",
"-c",
"from importlib.util import find_spec; "
"assert find_spec('fastjsonschema') is None, "
"'fastjsonschema leaked into the required-dependencies environment'",
)

# Execute test in specific folders
session.run("pytest", *folders)
session.run("pytest", *folders, *pytest_args)


@nox.session()
Expand All @@ -72,6 +86,7 @@ def test_with_only_required_packages(session: nox.Session):
f"{PREFIX_TESTS_FUNCTIONAL}/batch/required_dependencies/",
f"{PREFIX_TESTS_FUNCTIONAL}/kafka_consumer/required_dependencies/",
],
pytest_args=("-m", "not requires_validation"),
)


Expand Down Expand Up @@ -121,6 +136,7 @@ def test_with_boto3_sdk_as_required_package(session: nox.Session):
f"{PREFIX_TESTS_FUNCTIONAL}/idempotency/_boto3/",
],
extras="aws-sdk",
pytest_args=("-m", "not requires_validation"),
)


Expand All @@ -132,8 +148,10 @@ def test_with_fastjsonschema_as_required_package(session: nox.Session):
session,
folders=[
f"{PREFIX_TESTS_FUNCTIONAL}/validator/_fastjsonschema/",
f"{PREFIX_TESTS_FUNCTIONAL}/event_handler/required_dependencies/test_api_middlewares.py",
],
extras="validation",
pytest_args=("-m", "requires_validation"),
)


Expand Down Expand Up @@ -170,6 +188,29 @@ def test_with_pydantic_required_package(session: nox.Session):
f"{PREFIX_TESTS_UNIT}/event_handler/_pydantic/",
],
extras="parser",
pytest_args=("-m", "not requires_validation"),
)


@nox.session()
def test_with_parser_and_validation_required_packages(session: nox.Session):
"""Validate generated OpenAPI schemas with Parser and Validation dependencies."""
build_and_run_test(
session,
folders=[f"{PREFIX_TESTS_FUNCTIONAL}/event_handler/_pydantic/"],
extras="parser,validation",
pytest_args=("-m", "requires_validation"),
)


@nox.session()
def test_with_boto3_and_validation_required_packages(session: nox.Session):
"""Test Idempotency combined with the Validation decorator."""
build_and_run_test(
session,
folders=[f"{PREFIX_TESTS_FUNCTIONAL}/idempotency/_boto3/"],
extras="aws-sdk,validation",
pytest_args=("-m", "requires_validation"),
)


Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ addopts = "-ra -vv"
testpaths = "./tests"
markers = [
"perf: marks perf tests to be deselected (deselect with '-m \"not perf\"')",
"requires_validation: tests that also require the validation extra",
]

[tool.ty]
Expand Down
5 changes: 4 additions & 1 deletion tests/functional/event_handler/_pydantic/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import json

import fastjsonschema
import pytest

from aws_lambda_powertools.event_handler.openapi.models import APIKey, APIKeyIn
Expand Down Expand Up @@ -99,6 +98,8 @@ def pydanticv2_only():
def openapi30_schema():
from urllib.request import urlopen

import fastjsonschema

f = urlopen("https://spec.openapis.org/oas/3.0/schema/2021-09-28")
data = json.loads(f.read().decode("utf-8"))
return fastjsonschema.compile(
Expand All @@ -111,6 +112,8 @@ def openapi30_schema():
def openapi31_schema():
from urllib.request import urlopen

import fastjsonschema

f = urlopen("https://spec.openapis.org/oas/3.1/schema/2022-10-07")
data = json.loads(f.read().decode("utf-8"))
return fastjsonschema.compile(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ def send_reminders(


@pytest.mark.usefixtures("pydanticv2_only")
@pytest.mark.requires_validation
def test_openapi_schema_for_pydanticv2(openapi30_schema):
# GIVEN BedrockAgentResolver is initialized with enable_validation=True
app = BedrockAgentResolver(enable_validation=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@


@pytest.mark.usefixtures("pydanticv2_only")
@pytest.mark.requires_validation
def test_openapi_3_1_simple_handler(openapi31_schema):
# GIVEN APIGatewayRestResolver is initialized with enable_validation=True
app = APIGatewayRestResolver(enable_validation=True)
Expand Down Expand Up @@ -45,6 +46,7 @@ def test_openapi_3_0_with_pydantic_v2():


@pytest.mark.usefixtures("pydanticv2_only")
@pytest.mark.requires_validation
def test_openapi_3_1_complex_handler(openapi31_schema):
# GIVEN APIGatewayRestResolver is initialized with enable_validation=True
app = APIGatewayRestResolver(enable_validation=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
BaseMiddlewareHandler,
NextMiddleware,
)
from aws_lambda_powertools.event_handler.middlewares.schema_validation import (
SchemaValidationMiddleware,
)
from tests.functional.utils import load_event

if TYPE_CHECKING:
Expand Down Expand Up @@ -236,7 +233,10 @@ def get_lambda(my_id: str, name: str) -> Response:
),
],
)
@pytest.mark.requires_validation
def test_pass_schema_validation(app: ApiGatewayResolver, event, validation_schema):
from aws_lambda_powertools.event_handler.middlewares.schema_validation import SchemaValidationMiddleware

@app.post("/my/path", middlewares=[SchemaValidationMiddleware(validation_schema)])
def post_lambda() -> Response:
return Response(200, content_types.TEXT_HTML, "path")
Expand Down Expand Up @@ -266,7 +266,10 @@ def post_lambda() -> Response:
),
],
)
@pytest.mark.requires_validation
def test_fail_schema_validation(app: ApiGatewayResolver, event, validation_schema):
from aws_lambda_powertools.event_handler.middlewares.schema_validation import SchemaValidationMiddleware

@app.post("/my/path", middlewares=[SchemaValidationMiddleware(validation_schema)])
def post_lambda() -> Response:
return Response(200, content_types.TEXT_HTML, "Should not be returned")
Expand Down Expand Up @@ -300,7 +303,10 @@ def post_lambda() -> Response:
),
],
)
@pytest.mark.requires_validation
def test_invalid_schema_validation(app: ApiGatewayResolver, event):
from aws_lambda_powertools.event_handler.middlewares.schema_validation import SchemaValidationMiddleware

@app.post("/my/path", middlewares=[SchemaValidationMiddleware(inbound_schema="schema.json")])
def post_lambda() -> Response:
return Response(200, content_types.TEXT_HTML, "Should not be returned")
Expand Down
Loading