From cfa9305dca0ff5f687166c38310d0b85953e4521 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:23:03 +0100 Subject: [PATCH 01/18] Add deprecated parameter with default to BaseRouter.get --- aws_lambda_powertools/event_handler/api_gateway.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 001fcceac72..ab70231b631 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -985,6 +985,7 @@ def get( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable[..., Any]] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: """Get route decorator with GET `method` From e0440dc268e727efbdf4649cb37b914258298e68 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:26:06 +0100 Subject: [PATCH 02/18] Add parameter with default to BaseRouter.route --- aws_lambda_powertools/event_handler/api_gateway.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index ab70231b631..a7633d60dee 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -925,6 +925,7 @@ def route( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable[..., Any]] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: raise NotImplementedError() From f6c9bbb2c096e50dac8a3ae6a058a2e1c2b80920 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:29:48 +0100 Subject: [PATCH 03/18] Pass deprecated param from .get() into .route() --- aws_lambda_powertools/event_handler/api_gateway.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index a7633d60dee..ae938522452 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1026,6 +1026,7 @@ def lambda_handler(event, context): security, openapi_extensions, middlewares, + deprecated, ) def post( From 9df005983ecae696e87cf8b12aaf32deae195d68 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:40:22 +0100 Subject: [PATCH 04/18] Add param and pass along for post, put, delete, patch, head --- aws_lambda_powertools/event_handler/api_gateway.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index ae938522452..141562dcbcc 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1045,6 +1045,7 @@ def post( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable[..., Any]] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: """Post route decorator with POST `method` @@ -1085,6 +1086,7 @@ def lambda_handler(event, context): security, openapi_extensions, middlewares, + deprecated, ) def put( @@ -1103,6 +1105,7 @@ def put( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable[..., Any]] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: """Put route decorator with PUT `method` @@ -1143,6 +1146,7 @@ def lambda_handler(event, context): security, openapi_extensions, middlewares, + deprecated, ) def delete( @@ -1161,6 +1165,7 @@ def delete( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable[..., Any]] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: """Delete route decorator with DELETE `method` @@ -1200,6 +1205,7 @@ def lambda_handler(event, context): security, openapi_extensions, middlewares, + deprecated, ) def patch( @@ -1218,6 +1224,7 @@ def patch( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: """Patch route decorator with PATCH `method` @@ -1260,6 +1267,7 @@ def lambda_handler(event, context): security, openapi_extensions, middlewares, + deprecated, ) def head( @@ -1278,6 +1286,7 @@ def head( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: """Head route decorator with HEAD `method` @@ -1319,6 +1328,7 @@ def lambda_handler(event, context): security, openapi_extensions, middlewares, + deprecated, ) def _push_processed_stack_frame(self, frame: str): From d8ca3ffbffaa1043e4fdb7982c7aa2542b79490a Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:57:09 +0100 Subject: [PATCH 05/18] Add param and pass along for ApiGatewayRestResolver.route --- aws_lambda_powertools/event_handler/api_gateway.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 141562dcbcc..75fae26f1eb 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -2612,6 +2612,7 @@ def route( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable[..., Any]] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: # NOTE: see #1552 for more context. return super().route( @@ -2630,6 +2631,7 @@ def route( security, openapi_extensions, middlewares, + deprecated, ) # Override _compile_regex to exclude trailing slashes for route resolution From 9d2512b022f4927f62a2415ec86035fbf365ef01 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:58:44 +0100 Subject: [PATCH 06/18] Ditto for Route.__init__, use when creating operation metadata --- aws_lambda_powertools/event_handler/api_gateway.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 75fae26f1eb..3f0ae020e52 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -310,6 +310,7 @@ def __init__( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable[..., Response]] | None = None, + deprecated: bool = False, ): """ @@ -350,6 +351,8 @@ def __init__( Additional OpenAPI extensions as a dictionary. middlewares: list[Callable[..., Response]] | None The list of route middlewares to be called in order. + deprecated: bool + Whether or not to mark this route as deprecated in the OpenAPI schema """ self.method = method.upper() self.path = "/" if path.strip() == "" else path @@ -374,6 +377,7 @@ def __init__( self.openapi_extensions = openapi_extensions self.middlewares = middlewares or [] self.operation_id = operation_id or self._generate_operation_id() + self.deprecated = deprecated # _middleware_stack_built is used to ensure the middleware stack is only built once. self._middleware_stack_built = False @@ -670,6 +674,10 @@ def _openapi_operation_metadata(self, operation_ids: set[str]) -> dict[str, Any] operation_ids.add(self.operation_id) operation["operationId"] = self.operation_id + # Mark as deprecated if necessary + if self.deprecated: + operation["deprecated"] = True + return operation @staticmethod From 669d655d8ab57a7d1658ab17e4af6095ab0e5738 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:59:38 +0100 Subject: [PATCH 07/18] Add param and pass along in ApiGatewayResolver.route --- aws_lambda_powertools/event_handler/api_gateway.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 3f0ae020e52..3a6ed2414c5 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1972,6 +1972,7 @@ def route( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable[..., Any]] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: """Route decorator includes parameter `method`""" @@ -2000,6 +2001,7 @@ def register_resolver(func: AnyCallableT) -> AnyCallableT: security, openapi_extensions, middlewares, + deprecated, ) # The more specific route wins. From e183b518c4ca52efd0b55a588abc995c892b3628 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 21:49:50 +0100 Subject: [PATCH 08/18] Add param and pass along in Router.route, workaround for include_router --- aws_lambda_powertools/event_handler/api_gateway.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 3a6ed2414c5..a28ab456787 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -2448,12 +2448,16 @@ def include_router(self, router: Router, prefix: str | None = None) -> None: # Middleware store the route without prefix, so we must not include prefix when grabbing middlewares = router._routes_with_middleware.get(route) + # Workaround to support backward-compatible interface + new_route = new_route[:-1] # positional arguments until `middlewares` parameter + deprecated: bool = route[-1] # see route_key in Router.route + # Need to use "type: ignore" here since mypy does not like a named parameter after # tuple expansion since may cause duplicate named parameters in the function signature. # In this case this is not possible since the tuple expansion is from a hashable source # and the `middlewares` list is a non-hashable structure so will never be included. # Still need to ignore for mypy checks or will cause failures (false-positive) - self.route(*new_route, middlewares=middlewares)(func) # type: ignore + self.route(*new_route, deprecated=deprecated, middlewares=middlewares)(func) # type: ignore @staticmethod def _get_fields_from_routes(routes: Sequence[Route]) -> list[ModelField]: @@ -2516,6 +2520,7 @@ def route( security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, middlewares: list[Callable[..., Any]] | None = None, + deprecated: bool = False, ) -> Callable[[AnyCallableT], AnyCallableT]: def register_route(func: AnyCallableT) -> AnyCallableT: # All dict keys needs to be hashable. So we'll need to do some conversions: @@ -2540,6 +2545,7 @@ def register_route(func: AnyCallableT) -> AnyCallableT: include_in_schema, frozen_security, fronzen_openapi_extensions, + deprecated, ) # Collate Middleware for routes From 37313d5707cbf0d6ac82c25ca0ffa2122d2e1773 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:14:54 +0100 Subject: [PATCH 09/18] Functional tests --- .../event_handler/_pydantic/test_openapi_params.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index 710627922f6..5baf8f0e3c9 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -101,7 +101,14 @@ def handler(user_id: str, include_extra: bool = False): def test_openapi_with_custom_params(): app = APIGatewayRestResolver() - @app.get("/users", summary="Get Users", operation_id="GetUsers", description="Get paginated users", tags=["Users"]) + @app.get( + "/users", + summary="Get Users", + operation_id="GetUsers", + description="Get paginated users", + tags=["Users"], + deprecated=True, + ) def handler( count: Annotated[ int, @@ -119,6 +126,7 @@ def handler( assert get.operationId == "GetUsers" assert get.description == "Get paginated users" assert get.tags == ["Users"] + assert get.deprecated is True parameter = get.parameters[0] assert parameter.required is False @@ -484,6 +492,7 @@ def handler( assert get.operationId == "GetUsers" assert get.description == "Get paginated users" assert get.tags == ["Users"] + assert get.deprecated is None parameter = get.parameters[0] assert parameter.required is False From 0dcef6e39f65a1b34511bcec7bc4a0e6038454a2 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:50:21 +0100 Subject: [PATCH 10/18] Formatting --- .../provider/cloudwatch_emf/cloudwatch.py | 3 --- .../utilities/parser/models/apigw_websocket.py | 1 + tests/unit/metrics/test_functions.py | 18 +++++++++--------- .../parser/_pydantic/test_apigw_websockets.py | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 50ad1871953..cd9a90a0d19 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -24,7 +24,6 @@ from aws_lambda_powertools.shared.functions import resolve_env_var_choice from aws_lambda_powertools.warnings import PowertoolsUserWarning - if TYPE_CHECKING: from aws_lambda_powertools.metrics.provider.cloudwatch_emf.types import CloudWatchEMFOutput from aws_lambda_powertools.metrics.types import MetricNameUnitResolution @@ -295,8 +294,6 @@ def add_dimension(self, name: str, value: str) -> None: self.dimension_set[name] = value - - def add_metadata(self, key: str, value: Any) -> None: """Adds high cardinal metadata for metrics object diff --git a/aws_lambda_powertools/utilities/parser/models/apigw_websocket.py b/aws_lambda_powertools/utilities/parser/models/apigw_websocket.py index 0655825e776..b9e7ecd68c7 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigw_websocket.py +++ b/aws_lambda_powertools/utilities/parser/models/apigw_websocket.py @@ -9,6 +9,7 @@ class APIGatewayWebSocketEventIdentity(BaseModel): source_ip: IPvAnyNetwork = Field(alias="sourceIp") user_agent: Optional[str] = Field(None, alias="userAgent") + class APIGatewayWebSocketEventRequestContextBase(BaseModel): extended_request_id: str = Field(alias="extendedRequestId") request_time: str = Field(alias="requestTime") diff --git a/tests/unit/metrics/test_functions.py b/tests/unit/metrics/test_functions.py index 142be729ae6..e7647852a49 100644 --- a/tests/unit/metrics/test_functions.py +++ b/tests/unit/metrics/test_functions.py @@ -1,6 +1,8 @@ -import pytest import warnings +import pytest + +from aws_lambda_powertools.metrics import Metrics from aws_lambda_powertools.metrics.functions import ( extract_cloudwatch_metric_resolution_value, extract_cloudwatch_metric_unit_value, @@ -10,9 +12,9 @@ MetricUnitError, ) from aws_lambda_powertools.metrics.provider.cloudwatch_emf.metric_properties import MetricResolution, MetricUnit -from aws_lambda_powertools.metrics import Metrics from aws_lambda_powertools.warnings import PowertoolsUserWarning + @pytest.fixture def warning_catcher(monkeypatch): caught_warnings = [] @@ -20,7 +22,7 @@ def warning_catcher(monkeypatch): def custom_warn(message, category=None, stacklevel=1, source=None): caught_warnings.append(PowertoolsUserWarning(message)) - monkeypatch.setattr(warnings, 'warn', custom_warn) + monkeypatch.setattr(warnings, "warn", custom_warn) return caught_warnings @@ -78,13 +80,13 @@ def test_extract_valid_cloudwatch_metric_unit_value(): def test_add_dimension_overwrite_warning(warning_catcher): """ - Adds a dimension and then tries to add another with the same name - but a different value. Verifies if the dimension is updated with - the new value and warning is issued when an existing dimension + Adds a dimension and then tries to add another with the same name + but a different value. Verifies if the dimension is updated with + the new value and warning is issued when an existing dimension is overwritten. """ metrics = Metrics(namespace="TestNamespace") - + # GIVEN default dimension dimension_name = "test-dimension" value1 = "test-value-1" @@ -100,5 +102,3 @@ def test_add_dimension_overwrite_warning(warning_catcher): # AND a warning should be issued with the exact message expected_warning = f"Dimension '{dimension_name}' has already been added. The previous value will be overwritten." assert any(str(w) == expected_warning for w in warning_catcher) - - diff --git a/tests/unit/parser/_pydantic/test_apigw_websockets.py b/tests/unit/parser/_pydantic/test_apigw_websockets.py index aea77217d93..7b8a3c9ba46 100644 --- a/tests/unit/parser/_pydantic/test_apigw_websockets.py +++ b/tests/unit/parser/_pydantic/test_apigw_websockets.py @@ -114,4 +114,4 @@ def test_apigw_websocket_disconnect_event(): assert parsed_event.is_base64_encoded == raw_event["isBase64Encoded"] assert parsed_event.headers == raw_event["headers"] - assert parsed_event.multi_value_headers == raw_event["multiValueHeaders"] \ No newline at end of file + assert parsed_event.multi_value_headers == raw_event["multiValueHeaders"] From 67cf2c90f139ca578556529d9dcf89164630da07 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:31:41 +0100 Subject: [PATCH 11/18] Refactor to use defaultdict --- aws_lambda_powertools/event_handler/api_gateway.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index a28ab456787..c450bbfcc96 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -8,6 +8,7 @@ import warnings import zlib from abc import ABC, abstractmethod +from collections import defaultdict from enum import Enum from functools import partial from http import HTTPStatus @@ -2498,7 +2499,7 @@ class Router(BaseRouter): def __init__(self): self._routes: dict[tuple, Callable] = {} - self._routes_with_middleware: dict[tuple, list[Callable]] = {} + self._routes_with_middleware: defaultdict[tuple, list[Callable]] = defaultdict(list) self.api_resolver: BaseRouter | None = None self.context = {} # early init as customers might add context before event resolution self._exception_handlers: dict[type, Callable] = {} @@ -2551,12 +2552,7 @@ def register_route(func: AnyCallableT) -> AnyCallableT: # Collate Middleware for routes if middlewares is not None: for handler in middlewares: - if self._routes_with_middleware.get(route_key) is None: - self._routes_with_middleware[route_key] = [handler] - else: - self._routes_with_middleware[route_key].append(handler) - else: - self._routes_with_middleware[route_key] = [] + self._routes_with_middleware[route_key].append(handler) self._routes[route_key] = func From a5570137b448590e513673c2db7a73b31dc7864f Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Wed, 18 Dec 2024 00:51:41 +0100 Subject: [PATCH 12/18] Move deprecated operation tests into separate test case --- .../_pydantic/test_openapi_params.py | 60 +++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index 5baf8f0e3c9..f81748fe728 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -44,6 +44,7 @@ def handler(): get = path.get assert get.summary == "GET /" assert get.operationId == "handler__get" + assert get.deprecated is None assert get.responses is not None assert 200 in get.responses.keys() @@ -101,14 +102,7 @@ def handler(user_id: str, include_extra: bool = False): def test_openapi_with_custom_params(): app = APIGatewayRestResolver() - @app.get( - "/users", - summary="Get Users", - operation_id="GetUsers", - description="Get paginated users", - tags=["Users"], - deprecated=True, - ) + @app.get("/users", summary="Get Users", operation_id="GetUsers", description="Get paginated users", tags=["Users"]) def handler( count: Annotated[ int, @@ -126,7 +120,6 @@ def handler( assert get.operationId == "GetUsers" assert get.description == "Get paginated users" assert get.tags == ["Users"] - assert get.deprecated is True parameter = get.parameters[0] assert parameter.required is False @@ -396,6 +389,54 @@ def handler(user: Annotated[User, Body(description="This is a user")]): assert request_body.content[JSON_CONTENT_TYPE].schema_.description == "This is a user" +def test_openapi_with_deprecated_operations(): + app = APIGatewayRestResolver() + + @app.get("/", deprecated=True) + def _get(): + raise NotImplementedError() + + @app.post("/", deprecated=True) + def _post(): + raise NotImplementedError() + + @app.put("/", deprecated=True) + def _put(): + raise NotImplementedError() + + @app.delete("/", deprecated=True) + def _delete(): + raise NotImplementedError() + + @app.patch("/", deprecated=True) + def _patch(): + raise NotImplementedError() + + @app.head("/", deprecated=True) + def _head(): + raise NotImplementedError() + + schema = app.get_openapi_schema() + + get = schema.paths["/"].get + assert get.deprecated is True + + post = schema.paths["/"].post + assert post.deprecated is True + + put = schema.paths["/"].put + assert put.deprecated is True + + delete = schema.paths["/"].delete + assert delete.deprecated is True + + patch = schema.paths["/"].patch + assert patch.deprecated is True + + head = schema.paths["/"].head + assert head.deprecated is True + + def test_openapi_with_excluded_operations(): app = APIGatewayRestResolver() @@ -492,7 +533,6 @@ def handler( assert get.operationId == "GetUsers" assert get.description == "Get paginated users" assert get.tags == ["Users"] - assert get.deprecated is None parameter = get.parameters[0] assert parameter.required is False From f74b3848268c64d6ae8a27141be6b9f1aba93e2c Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Wed, 18 Dec 2024 00:53:00 +0100 Subject: [PATCH 13/18] Simplify test case --- .../_pydantic/test_openapi_params.py | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index f81748fe728..b51efd8fbc5 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -393,27 +393,7 @@ def test_openapi_with_deprecated_operations(): app = APIGatewayRestResolver() @app.get("/", deprecated=True) - def _get(): - raise NotImplementedError() - - @app.post("/", deprecated=True) - def _post(): - raise NotImplementedError() - - @app.put("/", deprecated=True) - def _put(): - raise NotImplementedError() - - @app.delete("/", deprecated=True) - def _delete(): - raise NotImplementedError() - - @app.patch("/", deprecated=True) - def _patch(): - raise NotImplementedError() - - @app.head("/", deprecated=True) - def _head(): + def handler(): raise NotImplementedError() schema = app.get_openapi_schema() @@ -421,21 +401,6 @@ def _head(): get = schema.paths["/"].get assert get.deprecated is True - post = schema.paths["/"].post - assert post.deprecated is True - - put = schema.paths["/"].put - assert put.deprecated is True - - delete = schema.paths["/"].delete - assert delete.deprecated is True - - patch = schema.paths["/"].patch - assert patch.deprecated is True - - head = schema.paths["/"].head - assert head.deprecated is True - def test_openapi_with_excluded_operations(): app = APIGatewayRestResolver() From 3886600ed1bf13e453badf25a84a92ce14568a04 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:50:29 +0100 Subject: [PATCH 14/18] Put 'deprecated' param before 'middlewares' --- .../event_handler/api_gateway.py | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index c450bbfcc96..0d032946aaa 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -310,8 +310,8 @@ def __init__( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable[..., Response]] | None = None, deprecated: bool = False, + middlewares: list[Callable[..., Response]] | None = None, ): """ @@ -350,10 +350,10 @@ def __init__( The OpenAPI security for this route openapi_extensions: dict[str, Any], optional Additional OpenAPI extensions as a dictionary. - middlewares: list[Callable[..., Response]] | None - The list of route middlewares to be called in order. deprecated: bool Whether or not to mark this route as deprecated in the OpenAPI schema + middlewares: list[Callable[..., Response]] | None + The list of route middlewares to be called in order. """ self.method = method.upper() self.path = "/" if path.strip() == "" else path @@ -933,8 +933,8 @@ def route( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable[..., Any]] | None = None, deprecated: bool = False, + middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: raise NotImplementedError() @@ -994,8 +994,8 @@ def get( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable[..., Any]] | None = None, deprecated: bool = False, + middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Get route decorator with GET `method` @@ -1034,8 +1034,8 @@ def lambda_handler(event, context): include_in_schema, security, openapi_extensions, - middlewares, deprecated, + middlewares, ) def post( @@ -1053,8 +1053,8 @@ def post( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable[..., Any]] | None = None, deprecated: bool = False, + middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Post route decorator with POST `method` @@ -1094,8 +1094,8 @@ def lambda_handler(event, context): include_in_schema, security, openapi_extensions, - middlewares, deprecated, + middlewares, ) def put( @@ -1113,8 +1113,8 @@ def put( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable[..., Any]] | None = None, deprecated: bool = False, + middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Put route decorator with PUT `method` @@ -1154,8 +1154,8 @@ def lambda_handler(event, context): include_in_schema, security, openapi_extensions, - middlewares, deprecated, + middlewares, ) def delete( @@ -1173,8 +1173,8 @@ def delete( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable[..., Any]] | None = None, deprecated: bool = False, + middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Delete route decorator with DELETE `method` @@ -1213,8 +1213,8 @@ def lambda_handler(event, context): include_in_schema, security, openapi_extensions, - middlewares, deprecated, + middlewares, ) def patch( @@ -1232,8 +1232,8 @@ def patch( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable] | None = None, deprecated: bool = False, + middlewares: list[Callable] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Patch route decorator with PATCH `method` @@ -1275,8 +1275,8 @@ def lambda_handler(event, context): include_in_schema, security, openapi_extensions, - middlewares, deprecated, + middlewares, ) def head( @@ -1294,8 +1294,8 @@ def head( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable] | None = None, deprecated: bool = False, + middlewares: list[Callable] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Head route decorator with HEAD `method` @@ -1336,8 +1336,8 @@ def lambda_handler(event, context): include_in_schema, security, openapi_extensions, - middlewares, deprecated, + middlewares, ) def _push_processed_stack_frame(self, frame: str): @@ -1651,7 +1651,6 @@ def get_openapi_schema( # Add routes to the OpenAPI schema for route in all_routes: - if route.security and not _validate_openapi_security_parameters( security=route.security, security_schemes=security_schemes, @@ -1716,7 +1715,6 @@ def _get_openapi_security( @staticmethod def _determine_openapi_version(openapi_version: str): - # Pydantic V2 has no support for OpenAPI schema 3.0 if not openapi_version.startswith("3.1"): warnings.warn( @@ -1972,8 +1970,8 @@ def route( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable[..., Any]] | None = None, deprecated: bool = False, + middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: """Route decorator includes parameter `method`""" @@ -2001,8 +1999,8 @@ def register_resolver(func: AnyCallableT) -> AnyCallableT: include_in_schema, security, openapi_extensions, - middlewares, deprecated, + middlewares, ) # The more specific route wins. @@ -2520,8 +2518,8 @@ def route( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable[..., Any]] | None = None, deprecated: bool = False, + middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: def register_route(func: AnyCallableT) -> AnyCallableT: # All dict keys needs to be hashable. So we'll need to do some conversions: @@ -2623,8 +2621,8 @@ def route( include_in_schema: bool = True, security: list[dict[str, list[str]]] | None = None, openapi_extensions: dict[str, Any] | None = None, - middlewares: list[Callable[..., Any]] | None = None, deprecated: bool = False, + middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[AnyCallableT], AnyCallableT]: # NOTE: see #1552 for more context. return super().route( @@ -2642,8 +2640,8 @@ def route( include_in_schema, security, openapi_extensions, - middlewares, deprecated, + middlewares, ) # Override _compile_regex to exclude trailing slashes for route resolution From 3375e1d57401fa42964ff9344f51f1518418d8bb Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:56:13 +0100 Subject: [PATCH 15/18] Remove workaround --- aws_lambda_powertools/event_handler/api_gateway.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 0d032946aaa..8cc43fbf615 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -2447,16 +2447,12 @@ def include_router(self, router: Router, prefix: str | None = None) -> None: # Middleware store the route without prefix, so we must not include prefix when grabbing middlewares = router._routes_with_middleware.get(route) - # Workaround to support backward-compatible interface - new_route = new_route[:-1] # positional arguments until `middlewares` parameter - deprecated: bool = route[-1] # see route_key in Router.route - # Need to use "type: ignore" here since mypy does not like a named parameter after # tuple expansion since may cause duplicate named parameters in the function signature. # In this case this is not possible since the tuple expansion is from a hashable source # and the `middlewares` list is a non-hashable structure so will never be included. # Still need to ignore for mypy checks or will cause failures (false-positive) - self.route(*new_route, deprecated=deprecated, middlewares=middlewares)(func) # type: ignore + self.route(*new_route, middlewares=middlewares)(func) # type: ignore @staticmethod def _get_fields_from_routes(routes: Sequence[Route]) -> list[ModelField]: From 6e9a5daed8e9c02a76ae3e47574cdfdccddd4c87 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:02:17 +0100 Subject: [PATCH 16/18] Add test case for deprecated POST operation --- .../event_handler/_pydantic/test_openapi_params.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index b51efd8fbc5..b561e389c5a 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -393,7 +393,11 @@ def test_openapi_with_deprecated_operations(): app = APIGatewayRestResolver() @app.get("/", deprecated=True) - def handler(): + def _get(): + raise NotImplementedError() + + @app.post("/", deprecated=True) + def _post(): raise NotImplementedError() schema = app.get_openapi_schema() @@ -401,6 +405,9 @@ def handler(): get = schema.paths["/"].get assert get.deprecated is True + post = schema.paths["/"].post + assert post.deprecated is True + def test_openapi_with_excluded_operations(): app = APIGatewayRestResolver() From d3ead97272937abff9c3f862e4eeca96abe15df6 Mon Sep 17 00:00:00 2001 From: Aleksei Tcysin <24791800+tcysin@users.noreply.github.com> Date: Thu, 19 Dec 2024 21:50:35 +0100 Subject: [PATCH 17/18] Add 'deprecated' param to BedrockAgentResolver methods --- aws_lambda_powertools/event_handler/bedrock_agent.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/event_handler/bedrock_agent.py b/aws_lambda_powertools/event_handler/bedrock_agent.py index 8af5520a188..215199e0022 100644 --- a/aws_lambda_powertools/event_handler/bedrock_agent.py +++ b/aws_lambda_powertools/event_handler/bedrock_agent.py @@ -108,9 +108,9 @@ def get( # type: ignore[override] tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, + deprecated: bool = False, middlewares: list[Callable[..., Any]] | None = None, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: - openapi_extensions = None security = None @@ -128,6 +128,7 @@ def get( # type: ignore[override] include_in_schema, security, openapi_extensions, + deprecated, middlewares, ) @@ -146,6 +147,7 @@ def post( # type: ignore[override] tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, + deprecated: bool = False, middlewares: list[Callable[..., Any]] | None = None, ): openapi_extensions = None @@ -165,6 +167,7 @@ def post( # type: ignore[override] include_in_schema, security, openapi_extensions, + deprecated, middlewares, ) @@ -183,6 +186,7 @@ def put( # type: ignore[override] tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, + deprecated: bool = False, middlewares: list[Callable[..., Any]] | None = None, ): openapi_extensions = None @@ -202,6 +206,7 @@ def put( # type: ignore[override] include_in_schema, security, openapi_extensions, + deprecated, middlewares, ) @@ -220,6 +225,7 @@ def patch( # type: ignore[override] tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, + deprecated: bool = False, middlewares: list[Callable] | None = None, ): openapi_extensions = None @@ -239,6 +245,7 @@ def patch( # type: ignore[override] include_in_schema, security, openapi_extensions, + deprecated, middlewares, ) @@ -257,6 +264,7 @@ def delete( # type: ignore[override] tags: list[str] | None = None, operation_id: str | None = None, include_in_schema: bool = True, + deprecated: bool = False, middlewares: list[Callable[..., Any]] | None = None, ): openapi_extensions = None @@ -276,6 +284,7 @@ def delete( # type: ignore[override] include_in_schema, security, openapi_extensions, + deprecated, middlewares, ) From 9968d681f67765f0932062a310cf32ca56911371 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 19 Dec 2024 22:31:11 +0000 Subject: [PATCH 18/18] Small changes + trigger pipeline --- .../event_handler/api_gateway.py | 13 +++++++----- .../_openapi_customization_operations.md | 1 + .../_pydantic/test_openapi_params.py | 20 +++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 8cc43fbf615..f4ef22019e5 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -8,7 +8,6 @@ import warnings import zlib from abc import ABC, abstractmethod -from collections import defaultdict from enum import Enum from functools import partial from http import HTTPStatus @@ -676,8 +675,7 @@ def _openapi_operation_metadata(self, operation_ids: set[str]) -> dict[str, Any] operation["operationId"] = self.operation_id # Mark as deprecated if necessary - if self.deprecated: - operation["deprecated"] = True + operation["deprecated"] = self.deprecated or None return operation @@ -2493,7 +2491,7 @@ class Router(BaseRouter): def __init__(self): self._routes: dict[tuple, Callable] = {} - self._routes_with_middleware: defaultdict[tuple, list[Callable]] = defaultdict(list) + self._routes_with_middleware: dict[tuple, list[Callable]] = {} self.api_resolver: BaseRouter | None = None self.context = {} # early init as customers might add context before event resolution self._exception_handlers: dict[type, Callable] = {} @@ -2546,7 +2544,12 @@ def register_route(func: AnyCallableT) -> AnyCallableT: # Collate Middleware for routes if middlewares is not None: for handler in middlewares: - self._routes_with_middleware[route_key].append(handler) + if self._routes_with_middleware.get(route_key) is None: + self._routes_with_middleware[route_key] = [handler] + else: + self._routes_with_middleware[route_key].append(handler) + else: + self._routes_with_middleware[route_key] = [] self._routes[route_key] = func diff --git a/docs/core/event_handler/_openapi_customization_operations.md b/docs/core/event_handler/_openapi_customization_operations.md index df842b2b7fc..0072ec1fae4 100644 --- a/docs/core/event_handler/_openapi_customization_operations.md +++ b/docs/core/event_handler/_openapi_customization_operations.md @@ -13,3 +13,4 @@ Here's a breakdown of various customizable fields: | `tags` | `List[str]` | Tags are a way to categorize and group endpoints within the API documentation. They can help organize the operations by resources or other heuristic. | | `operation_id` | `str` | A unique identifier for the operation, which can be used for referencing this operation in documentation or code. This ID must be unique across all operations described in the API. | | `include_in_schema` | `bool` | A boolean value that determines whether or not this operation should be included in the OpenAPI schema. Setting it to `False` can hide the endpoint from generated documentation and schema exports, which might be useful for private or experimental endpoints. | +| `deprecated` | `bool` | A boolean value that determines whether or not this operation should be marked as deprecated in the OpenAPI schema. | diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index b561e389c5a..a57156db130 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -409,6 +409,26 @@ def _post(): assert post.deprecated is True +def test_openapi_without_deprecated_operations(): + app = APIGatewayRestResolver() + + @app.get("/") + def _get(): + raise NotImplementedError() + + @app.post("/", deprecated=False) + def _post(): + raise NotImplementedError() + + schema = app.get_openapi_schema() + + get = schema.paths["/"].get + assert get.deprecated is None + + post = schema.paths["/"].post + assert post.deprecated is None + + def test_openapi_with_excluded_operations(): app = APIGatewayRestResolver()