Skip to content

Commit 5cc265e

Browse files
Merge branch 'develop' into develop
2 parents 4400181 + 810aadc commit 5cc265e

File tree

11 files changed

+639
-97
lines changed

11 files changed

+639
-97
lines changed

CHANGELOG.md

Lines changed: 147 additions & 0 deletions
Large diffs are not rendered by default.

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -661,30 +661,36 @@ def _get_openapi_path( # noqa PLR0912
661661
else:
662662
# Need to iterate to transform any 'model' into a 'schema'
663663
for content_type, payload in response["content"].items():
664-
new_payload: OpenAPIResponseContentSchema
665-
666664
# Case 2.1: the 'content' has a model
667665
if "model" in payload:
668666
# Find the model in the dependant's extra models
667+
model_payload_typed = cast(OpenAPIResponseContentModel, payload)
669668
return_field = next(
670669
filter(
671-
lambda model: model.type_ is cast(OpenAPIResponseContentModel, payload)["model"],
670+
lambda model: model.type_ is model_payload_typed["model"],
672671
self.dependant.response_extra_models,
673672
),
674673
)
675674
if not return_field:
676675
raise AssertionError("Model declared in custom responses was not found")
677676

678-
new_payload = self._openapi_operation_return(
677+
model_payload = self._openapi_operation_return(
679678
param=return_field,
680679
model_name_map=model_name_map,
681680
field_mapping=field_mapping,
682681
)
683682

683+
# Preserve existing fields like examples, encoding, etc.
684+
new_payload: OpenAPIResponseContentSchema = {}
685+
for key, value in payload.items():
686+
if key != "model":
687+
new_payload[key] = value # type: ignore[literal-required]
688+
new_payload.update(model_payload) # Add/override with model schema
689+
684690
# Case 2.2: the 'content' has a schema
685691
else:
686692
# Do nothing! We already have what we need!
687-
new_payload = payload
693+
new_payload = cast(OpenAPIResponseContentSchema, payload)
688694

689695
response["content"][content_type] = new_payload
690696

aws_lambda_powertools/event_handler/openapi/types.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,32 @@
6363
}
6464

6565

66+
class OpenAPIResponseHeader(TypedDict, total=False):
67+
"""OpenAPI Response Header Object"""
68+
69+
description: NotRequired[str]
70+
schema: NotRequired[dict[str, Any]]
71+
examples: NotRequired[dict[str, Any]]
72+
style: NotRequired[str]
73+
explode: NotRequired[bool]
74+
allowReserved: NotRequired[bool]
75+
deprecated: NotRequired[bool]
76+
77+
6678
class OpenAPIResponseContentSchema(TypedDict, total=False):
6779
schema: dict
80+
examples: NotRequired[dict[str, Any]]
81+
encoding: NotRequired[dict[str, Any]]
6882

6983

70-
class OpenAPIResponseContentModel(TypedDict):
84+
class OpenAPIResponseContentModel(TypedDict, total=False):
7185
model: Any
86+
examples: NotRequired[dict[str, Any]]
87+
encoding: NotRequired[dict[str, Any]]
7288

7389

74-
class OpenAPIResponse(TypedDict):
75-
description: str
90+
class OpenAPIResponse(TypedDict, total=False):
91+
description: str # Still required
92+
headers: NotRequired[dict[str, OpenAPIResponseHeader]]
7693
content: NotRequired[dict[str, OpenAPIResponseContentSchema | OpenAPIResponseContentModel]]
94+
links: NotRequired[dict[str, Any]]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Exposes version constant to avoid circular dependencies."""
22

3-
VERSION = "3.20.0"
3+
VERSION = "3.20.1a0"
Lines changed: 187 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,109 @@
11
from typing import Any, Dict, List, Optional, Union
22

3-
from pydantic import BaseModel
3+
from pydantic import BaseModel, Field
44

55

66
class AppSyncIamIdentity(BaseModel):
7-
accountId: str
8-
cognitoIdentityPoolId: Optional[str]
9-
cognitoIdentityId: Optional[str]
10-
sourceIp: List[str]
11-
username: str
12-
userArn: str
13-
cognitoIdentityAuthType: Optional[str]
14-
cognitoIdentityAuthProvider: Optional[str]
7+
accountId: str = Field(description="The AWS account ID of the caller.", examples=["123456789012"])
8+
cognitoIdentityPoolId: Optional[str] = Field(
9+
default=None,
10+
description="The Amazon Cognito identity pool ID associated with the caller.",
11+
examples=["us-east-1:12345678-1234-1234-1234-123456789012"],
12+
)
13+
cognitoIdentityId: Optional[str] = Field(
14+
default=None,
15+
description="The Amazon Cognito identity ID of the caller.",
16+
examples=["us-east-1:12345678-1234-1234-1234-123456789012"],
17+
)
18+
sourceIp: List[str] = Field(
19+
description=(
20+
"The source IP address of the caller that AWS AppSync receives. "
21+
"If the request includes a x-forwarded-for header, this is a list of IP addresses."
22+
),
23+
)
24+
username: str = Field(
25+
description="The IAM user principal name.",
26+
examples=["AIDAJEXAMPLE1234", "appsync-user"],
27+
)
28+
userArn: str = Field(
29+
description="The Amazon Resource Name (ARN) of the IAM user.",
30+
examples=["arn:aws:iam::123456789012:user/appsync", "arn:aws:iam::123456789012:user/service-user"],
31+
)
32+
cognitoIdentityAuthType: Optional[str] = Field(
33+
default=None,
34+
description="Either authenticated or unauthenticated based on the identity type.",
35+
examples=["authenticated", "unauthenticated"],
36+
)
37+
cognitoIdentityAuthProvider: Optional[str] = Field(
38+
default=None,
39+
description=(
40+
"A comma-separated list of external identity provider information "
41+
"used in obtaining the credentials used to sign the request."
42+
),
43+
examples=[
44+
"cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID",
45+
"graph.facebook.com,cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID",
46+
],
47+
)
1548

1649

1750
class AppSyncCognitoIdentity(BaseModel):
18-
sub: str
19-
issuer: str
20-
username: str
21-
claims: Dict[str, Any]
22-
sourceIp: List[str]
23-
defaultAuthStrategy: str
24-
groups: Optional[List[str]]
51+
sub: str = Field(
52+
description="The UUID of the authenticated user from Cognito User Pool.",
53+
examples=["user-uuid-1234-5678-9012-123456789012", "user-uuid-abcd-efgh-ijkl-mnopqrstuvwx"],
54+
)
55+
issuer: str = Field(
56+
description="The token issuer URL from Cognito User Pool.",
57+
examples=[
58+
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID",
59+
"https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx",
60+
],
61+
)
62+
username: str = Field(
63+
description="The username of the authenticated user (cognito:username attribute).",
64+
examples=["mike", "jdoe", "user123"],
65+
)
66+
claims: Dict[str, Any] = Field(description="The JWT claims that the user has from Cognito User Pool.")
67+
sourceIp: List[str] = Field(
68+
description=(
69+
"The source IP address of the caller that AWS AppSync receives. "
70+
"If the request includes a x-forwarded-for header, this is a list of IP addresses."
71+
),
72+
)
73+
defaultAuthStrategy: str = Field(
74+
description="The default authorization strategy for this caller (ALLOW or DENY).",
75+
examples=["ALLOW", "DENY"],
76+
)
77+
groups: Optional[List[str]] = Field(
78+
default=None,
79+
description="The Cognito User Pool groups that the user belongs to.",
80+
examples=[["admin", "users"], ["developers"]],
81+
)
2582

2683

2784
class AppSyncOidcIdentity(BaseModel):
28-
claims: Dict[str, Any]
29-
issuer: str
30-
sub: str
85+
claims: Dict[str, Any] = Field(description="The JWT claims from the OpenID Connect provider.")
86+
issuer: str = Field(
87+
description="The token issuer URL from the OpenID Connect provider.",
88+
examples=["https://accounts.google.com", "https://login.microsoftonline.com/tenant-id/v2.0"],
89+
)
90+
sub: str = Field(
91+
description="The subject identifier from the OpenID Connect provider.",
92+
examples=["248289761001", "provider-subject-identifier"],
93+
)
3194

3295

3396
class AppSyncLambdaIdentity(BaseModel):
34-
resolverContext: Dict[str, Any]
97+
resolverContext: Dict[str, Any] = Field(
98+
description=(
99+
"The resolver context returned by the Lambda function authorizing the request. "
100+
"Contains custom authorization data from AWS_LAMBDA authorization."
101+
),
102+
examples=[
103+
{"userId": "user123", "role": "admin", "permissions": ["read", "write"]},
104+
{"customClaim": "value", "authLevel": "premium"},
105+
],
106+
)
35107

36108

37109
AppSyncIdentity = Union[
@@ -43,30 +115,110 @@ class AppSyncLambdaIdentity(BaseModel):
43115

44116

45117
class AppSyncRequestModel(BaseModel):
46-
domainName: Optional[str]
47-
headers: Dict[str, str]
118+
domainName: Optional[str] = Field(
119+
default=None,
120+
description=(
121+
"The custom domain name used to access the GraphQL endpoint. "
122+
"Returns null when using the default GraphQL endpoint domain name."
123+
),
124+
examples=["api.example.com", "graphql.mycompany.com"],
125+
)
126+
headers: Dict[str, str] = Field(
127+
description="HTTP headers from the GraphQL request, including custom headers.",
128+
examples=[
129+
{
130+
"cloudfront-viewer-country": "US",
131+
"host": "example.appsync-api.us-east-1.amazonaws.com",
132+
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
133+
"content-type": "application/json",
134+
},
135+
],
136+
)
48137

49138

50139
class AppSyncInfoModel(BaseModel):
51-
selectionSetList: List[str]
52-
selectionSetGraphQL: str
53-
parentTypeName: str
54-
fieldName: str
55-
variables: Dict[str, Any]
140+
selectionSetList: List[str] = Field(
141+
description=(
142+
"A list representation of the fields in the GraphQL selection set. "
143+
"Fields that are aliased are referenced only by the alias name."
144+
),
145+
examples=[["id", "field1", "field2"], ["postId", "title", "content", "author", "author/id", "author/name"]],
146+
)
147+
selectionSetGraphQL: str = Field(
148+
description=(
149+
"A string representation of the selection set, formatted as GraphQL SDL. "
150+
"Inline fragments are preserved but fragments are not merged."
151+
),
152+
examples=[
153+
"{\n id\n field1\n field2\n}",
154+
"{\n postId\n title\n content\n author {\n id\n name\n }\n}",
155+
],
156+
)
157+
parentTypeName: str = Field(
158+
description="The name of the parent type for the field that is currently being resolved.",
159+
examples=["Query", "Mutation", "Subscription", "User", "Post"],
160+
)
161+
fieldName: str = Field(
162+
description="The name of the field that is currently being resolved.",
163+
examples=["getUser", "createPost", "locations", "updateProfile"],
164+
)
165+
variables: Dict[str, Any] = Field(
166+
description="A map which holds all variables that are passed into the GraphQL request.",
167+
examples=[{"userId": "123", "limit": 10}, {"input": {"name": "John", "email": "[email protected]"}}, {}],
168+
)
56169

57170

58171
class AppSyncPrevModel(BaseModel):
59-
result: Dict[str, Any]
172+
result: Dict[str, Any] = Field(
173+
description=(
174+
"The result of whatever previous operation was executed in a pipeline resolver. "
175+
"Contains the output from the previous function or Before mapping template."
176+
),
177+
examples=[
178+
{"userId": "123", "posts": [{"id": "1", "title": "Hello World"}]},
179+
{"data": {"field1": "value1", "field2": "value2"}},
180+
],
181+
)
60182

61183

62184
class AppSyncResolverEventModel(BaseModel):
63-
arguments: Dict[str, Any]
64-
identity: Optional[AppSyncIdentity]
65-
source: Optional[Dict[str, Any]]
66-
request: AppSyncRequestModel
67-
info: AppSyncInfoModel
68-
prev: Optional[AppSyncPrevModel]
69-
stash: Dict[str, Any]
185+
arguments: Dict[str, Any] = Field(
186+
description="The arguments passed to the GraphQL field.",
187+
examples=[
188+
{"id": "123", "limit": 10},
189+
{"input": {"name": "John", "email": "[email protected]"}},
190+
{"page": 2, "size": 1, "name": "value"},
191+
],
192+
)
193+
identity: Optional[AppSyncIdentity] = Field(
194+
default=None,
195+
description="Information about the caller identity (authenticated user or API key).",
196+
)
197+
source: Optional[Dict[str, Any]] = Field(
198+
default=None,
199+
description="The parent object for the field. For top-level fields, this will be null.",
200+
examples=[
201+
None,
202+
{"id": "user123", "name": "John Doe"},
203+
{"name": "Value", "nested": {"name": "value", "list": []}},
204+
{"postId": "post456", "title": "My Post"},
205+
],
206+
)
207+
request: AppSyncRequestModel = Field(description="Information about the GraphQL request context.")
208+
info: AppSyncInfoModel = Field(
209+
description="Information about the GraphQL request including selection set and field details.",
210+
)
211+
prev: Optional[AppSyncPrevModel] = Field(
212+
default=None,
213+
description="Results from the previous resolver in a pipeline resolver.",
214+
)
215+
stash: Dict[str, Any] = Field(
216+
description=(
217+
"The stash is a map that is made available inside each resolver and function mapping template. "
218+
"The same stash instance lives through a single resolver execution."
219+
),
220+
examples=[{"customData": "value", "userId": "123"}],
221+
)
70222

71223

72224
AppSyncBatchResolverEventModel = List[AppSyncResolverEventModel]

0 commit comments

Comments
 (0)