Skip to content

Commit f096352

Browse files
Merge branch 'develop' into fix/8185
2 parents 7ecc159 + f83e141 commit f096352

6 files changed

Lines changed: 104 additions & 7 deletions

File tree

aws_lambda_powertools/event_handler/openapi/params.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -901,7 +901,7 @@ def analyze_param(
901901
if is_response_param:
902902
field_info.default = Required
903903

904-
field = _create_model_field(field_info, type_annotation, param_name, is_path_param)
904+
field = _create_model_field(field_info, type_annotation, param_name, is_path_param, is_response_param)
905905
return field
906906

907907

@@ -1138,6 +1138,7 @@ def _create_model_field(
11381138
type_annotation: Any,
11391139
param_name: str,
11401140
is_path_param: bool,
1141+
is_response_param: bool = False,
11411142
) -> ModelField | None:
11421143
"""
11431144
Create a new ModelField from a FieldInfo and type annotation.
@@ -1164,4 +1165,5 @@ def _create_model_field(
11641165
alias=field_info.alias,
11651166
required=field_info.default in (Required, Undefined),
11661167
field_info=field_info,
1168+
mode="serialization" if is_response_param else "validation",
11671169
)

aws_lambda_powertools/utilities/idempotency/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def _process_idempotency(self, is_replay: bool):
167167
# We give preference to ReturnValuesOnConditionCheckFailure because it is a faster and more cost-effective
168168
# way of retrieving the existing record after a failed conditional write operation.
169169
record = exc.old_data_record or self._get_idempotency_record()
170-
if is_replay and record is not None and record.status == "INPROGRESS":
170+
if is_replay and record is not None and record.status == STATUS_CONSTANTS["INPROGRESS"]:
171171
return self._get_function_response()
172172
# If a record is found, handle it for status
173173
if record:
@@ -296,7 +296,7 @@ def _get_function_response(self):
296296

297297
else:
298298
try:
299-
serialized_response: dict = self.output_serializer.to_dict(response) if response else None
299+
serialized_response: dict = self.output_serializer.to_dict(response) if response is not None else None
300300
self.persistence_store.save_success(data=self.data, result=serialized_response)
301301
except Exception as save_exception:
302302
raise IdempotencyPersistenceLayerError(

aws_lambda_powertools/utilities/idempotency/persistence/redis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,8 @@ def _item_to_data_record(self, idempotency_key: str, item: dict[str, Any]) -> Da
332332
idempotency_key=idempotency_key,
333333
status=item[self.status_attr],
334334
in_progress_expiry_timestamp=in_progress_expiry_timestamp,
335-
response_data=str(item.get(self.data_attr)),
336-
payload_hash=str(item.get(self.validation_key_attr)),
335+
response_data=item.get(self.data_attr, ""),
336+
payload_hash=item.get(self.validation_key_attr, ""),
337337
expiry_timestamp=item.get("expiration"),
338338
)
339339

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
aws-lambda-powertools[tracer]
2-
requests>=2.32.0
2+
requests>=2.33.1

tests/functional/event_handler/_pydantic/test_openapi_schema_pydantic_v2.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Literal, Optional
44

55
import pytest
6-
from pydantic import BaseModel, Field
6+
from pydantic import BaseModel, Field, computed_field
77
from typing_extensions import Annotated
88

99
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
@@ -110,3 +110,79 @@ def create_todo(todo: TodoEnvelope): ...
110110

111111
# THEN the schema should be valid
112112
assert openapi31_schema(schema)
113+
114+
115+
@pytest.mark.usefixtures("pydanticv2_only")
116+
def test_openapi_schema_includes_computed_field():
117+
# GIVEN a model with a computed_field
118+
class User(BaseModel):
119+
first_name: str
120+
last_name: str
121+
122+
@computed_field
123+
@property
124+
def full_name(self) -> str:
125+
return f"{self.first_name} {self.last_name}"
126+
127+
# GIVEN APIGatewayRestResolver with a handler returning that model
128+
app = APIGatewayRestResolver(enable_validation=True)
129+
130+
@app.get("/user")
131+
def get_user() -> User:
132+
return User(first_name="John", last_name="Doe")
133+
134+
# WHEN we get the schema
135+
schema = json.loads(app.get_openapi_json_schema())
136+
137+
# THEN the computed_field should appear in the response schema
138+
user_schema = schema["components"]["schemas"]["User"]
139+
assert "full_name" in user_schema["properties"]
140+
assert user_schema["properties"]["full_name"]["type"] == "string"
141+
assert user_schema["properties"]["full_name"].get("readOnly") is True
142+
143+
144+
@pytest.mark.usefixtures("pydanticv2_only")
145+
def test_openapi_schema_computed_field_not_in_request_body():
146+
# GIVEN a model with a computed_field used as both request and response
147+
class Item(BaseModel):
148+
price: float
149+
quantity: int
150+
151+
@computed_field
152+
@property
153+
def total(self) -> float:
154+
return self.price * self.quantity
155+
156+
# GIVEN APIGatewayRestResolver with handlers using the model
157+
app = APIGatewayRestResolver(enable_validation=True)
158+
159+
@app.post("/items")
160+
def create_item(item: Item) -> Item:
161+
return item
162+
163+
# WHEN we get the schema
164+
schema = json.loads(app.get_openapi_json_schema())
165+
166+
# THEN the request body schema should NOT include computed_field
167+
request_body = schema["paths"]["/items"]["post"]["requestBody"]
168+
request_ref = request_body["content"]["application/json"]["schema"]["$ref"]
169+
request_schema_name = request_ref.split("/")[-1]
170+
171+
# THEN the response schema SHOULD include computed_field
172+
response_ref = schema["paths"]["/items"]["post"]["responses"]["200"]["content"]["application/json"]["schema"][
173+
"$ref"
174+
]
175+
response_schema_name = response_ref.split("/")[-1]
176+
177+
# When input/output schemas are separate, we expect different schema names
178+
# When they share a schema, computed_field should be present
179+
if request_schema_name == response_schema_name:
180+
# Shared schema - computed_field should be present (serialization mode wins)
181+
item_schema = schema["components"]["schemas"][response_schema_name]
182+
assert "total" in item_schema["properties"]
183+
else:
184+
# Separate schemas
185+
input_schema = schema["components"]["schemas"][request_schema_name]
186+
output_schema = schema["components"]["schemas"][response_schema_name]
187+
assert "total" not in input_schema["properties"]
188+
assert "total" in output_schema["properties"]

tests/functional/idempotency/_redis/test_redis_layer.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,25 @@ def test_item_to_datarecord_conversion(valid_record):
330330
assert record.in_progress_expiry_timestamp == item[layer.in_progress_expiry_attr]
331331

332332

333+
def test_item_to_datarecord_conversion_missing_optional_attributes(persistence_store_standalone_redis):
334+
"""
335+
When data_attr or validation_key_attr is missing from Redis,
336+
response_data and payload_hash should be empty string, not the string "None".
337+
Regression test for: https://github.com/aws-powertools/powertools-lambda-python/issues/8090
338+
"""
339+
idempotency_key = "test-func#abc123"
340+
item = {
341+
persistence_store_standalone_redis.status_attr: "COMPLETED",
342+
persistence_store_standalone_redis.expiry_attr: 9999999999,
343+
# data_attr and validation_key_attr intentionally absent
344+
}
345+
346+
record = persistence_store_standalone_redis._item_to_data_record(idempotency_key, item)
347+
348+
assert record.response_data == ""
349+
assert record.payload_hash == ""
350+
351+
333352
def test_idempotent_function_and_lambda_handler_redis_basic(
334353
persistence_store_standalone_redis: RedisCachePersistenceLayer,
335354
lambda_context,

0 commit comments

Comments
 (0)