Skip to content

Commit 8daf90c

Browse files
pydantic v2: initial tests
1 parent 5ec1fa8 commit 8daf90c

20 files changed

+226
-119
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Code quality temp - Pydanticv2
2+
3+
# PROCESS
4+
#
5+
# 1. Install all dependencies and spin off containers for all supported Python versions
6+
# 2. Run code formatters and linters (various checks) for code standard
7+
# 3. Run static typing checker for potential bugs
8+
# 4. Run entire test suite for regressions except end-to-end (unit, functional, performance)
9+
# 5. Run static analysis (in addition to CodeQL) for common insecure code practices
10+
# 6. Run complexity baseline to avoid error-prone bugs and keep maintenance lower
11+
# 7. Collect and report on test coverage
12+
13+
# USAGE
14+
#
15+
# Always triggered on new PRs, PR changes and PR merge.
16+
17+
18+
on:
19+
pull_request:
20+
paths:
21+
- "aws_lambda_powertools/**"
22+
- "tests/**"
23+
- "pyproject.toml"
24+
- "poetry.lock"
25+
- "mypy.ini"
26+
branches:
27+
- poc/pydanticv2
28+
push:
29+
paths:
30+
- "aws_lambda_powertools/**"
31+
- "tests/**"
32+
- "pyproject.toml"
33+
- "poetry.lock"
34+
- "mypy.ini"
35+
branches:
36+
- poc/pydanticv2
37+
38+
permissions:
39+
contents: read
40+
41+
jobs:
42+
quality_check:
43+
runs-on: ubuntu-latest
44+
strategy:
45+
max-parallel: 4
46+
matrix:
47+
python-version: ["3.7", "3.8", "3.9", "3.10"]
48+
env:
49+
PYTHON: "${{ matrix.python-version }}"
50+
permissions:
51+
contents: read # checkout code only
52+
steps:
53+
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
54+
- name: Install poetry
55+
run: pipx install poetry
56+
- name: Set up Python ${{ matrix.python-version }}
57+
uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1
58+
with:
59+
python-version: ${{ matrix.python-version }}
60+
cache: "poetry"
61+
- name: Removing cfn-lint
62+
run: poetry remove cfn-lint
63+
- name: Replacing Pydantic v1 with v2
64+
run: poetry add "pydantic>=2.0"
65+
- name: Install dependencies
66+
run: make dev
67+
- name: Formatting and Linting
68+
run: make lint
69+
- name: Static type checking
70+
run: make mypy
71+
- name: Test with pytest
72+
run: make test
73+
- name: Security baseline
74+
run: make security-baseline
75+
- name: Complexity baseline
76+
run: make complexity-baseline
77+
- name: Upload coverage to Codecov
78+
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # 3.1.4
79+
with:
80+
file: ./coverage.xml
81+
env_vars: PYTHON
82+
name: aws-lambda-powertools-python-codecov

aws_lambda_powertools/shared/user_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def register_feature_to_session(session, feature):
112112
def register_feature_to_botocore_session(botocore_session, feature):
113113
"""
114114
Register the given feature string to the event system of the provided botocore session
115-
115+
116116
Please notice this function is for patching botocore session and is different from
117117
previous one which is for patching boto3 session
118118
@@ -127,7 +127,7 @@ def register_feature_to_botocore_session(botocore_session, feature):
127127
------
128128
AttributeError
129129
If the provided session does not have an event system.
130-
130+
131131
Examples
132132
--------
133133
**register data-masking user-agent to botocore session**
@@ -139,7 +139,7 @@ def register_feature_to_botocore_session(botocore_session, feature):
139139
>>> session = botocore.session.Session()
140140
>>> register_feature_to_botocore_session(botocore_session=session, feature="data-masking")
141141
>>> key_provider = StrictAwsKmsMasterKeyProvider(key_ids=self.keys, botocore_session=session)
142-
142+
143143
"""
144144
try:
145145
botocore_session.register(TARGET_SDK_EVENT, _create_feature_function(feature))
Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from datetime import datetime
1+
from datetime import datetime, timezone
22
from typing import Any, Dict, List, Optional, Type, Union
33

4-
from pydantic import BaseModel, root_validator
4+
from pydantic import BaseModel, root_validator, validator
55
from pydantic.networks import IPvAnyNetwork
66

77
from aws_lambda_powertools.utilities.parser.types import Literal
@@ -21,74 +21,79 @@ class ApiGatewayUserCert(BaseModel):
2121

2222

2323
class APIGatewayEventIdentity(BaseModel):
24-
accessKey: Optional[str]
25-
accountId: Optional[str]
26-
apiKey: Optional[str]
27-
apiKeyId: Optional[str]
28-
caller: Optional[str]
29-
cognitoAuthenticationProvider: Optional[str]
30-
cognitoAuthenticationType: Optional[str]
31-
cognitoIdentityId: Optional[str]
32-
cognitoIdentityPoolId: Optional[str]
33-
principalOrgId: Optional[str]
24+
accessKey: Optional[str] = None
25+
accountId: Optional[str] = None
26+
apiKey: Optional[str] = None
27+
apiKeyId: Optional[str] = None
28+
caller: Optional[str] = None
29+
cognitoAuthenticationProvider: Optional[str] = None
30+
cognitoAuthenticationType: Optional[str] = None
31+
cognitoIdentityId: Optional[str] = None
32+
cognitoIdentityPoolId: Optional[str] = None
33+
principalOrgId: Optional[str] = None
3434
# see #1562, temp workaround until API Gateway fixes it the Test button payload
3535
# removing it will not be considered a regression in the future
3636
sourceIp: Union[IPvAnyNetwork, Literal["test-invoke-source-ip"]]
37-
user: Optional[str]
38-
userAgent: Optional[str]
39-
userArn: Optional[str]
40-
clientCert: Optional[ApiGatewayUserCert]
37+
user: Optional[str] = None
38+
userAgent: Optional[str] = None
39+
userArn: Optional[str] = None
40+
clientCert: Optional[ApiGatewayUserCert] = None
4141

4242

4343
class APIGatewayEventAuthorizer(BaseModel):
44-
claims: Optional[Dict[str, Any]]
45-
scopes: Optional[List[str]]
44+
claims: Optional[Dict[str, Any]] = None
45+
scopes: Optional[List[str]] = None
4646

4747

4848
class APIGatewayEventRequestContext(BaseModel):
4949
accountId: str
5050
apiId: str
51-
authorizer: Optional[APIGatewayEventAuthorizer]
51+
authorizer: Optional[APIGatewayEventAuthorizer] = None
5252
stage: str
5353
protocol: str
5454
identity: APIGatewayEventIdentity
5555
requestId: str
5656
requestTime: str
5757
requestTimeEpoch: datetime
58-
resourceId: Optional[str]
58+
resourceId: Optional[str] = None
5959
resourcePath: str
60-
domainName: Optional[str]
61-
domainPrefix: Optional[str]
62-
extendedRequestId: Optional[str]
60+
domainName: Optional[str] = None
61+
domainPrefix: Optional[str] = None
62+
extendedRequestId: Optional[str] = None
6363
httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
6464
path: str
65-
connectedAt: Optional[datetime]
66-
connectionId: Optional[str]
67-
eventType: Optional[Literal["CONNECT", "MESSAGE", "DISCONNECT"]]
68-
messageDirection: Optional[str]
69-
messageId: Optional[str]
70-
routeKey: Optional[str]
71-
operationName: Optional[str]
72-
73-
@root_validator(allow_reuse=True)
65+
connectedAt: Optional[datetime] = None
66+
connectionId: Optional[str] = None
67+
eventType: Optional[Literal["CONNECT", "MESSAGE", "DISCONNECT"]] = None
68+
messageDirection: Optional[str] = None
69+
messageId: Optional[str] = None
70+
routeKey: Optional[str] = None
71+
operationName: Optional[str] = None
72+
73+
@root_validator(allow_reuse=True, skip_on_failure=True)
7474
def check_message_id(cls, values):
7575
message_id, event_type = values.get("messageId"), values.get("eventType")
7676
if message_id is not None and event_type != "MESSAGE":
77-
raise TypeError("messageId is available only when the `eventType` is `MESSAGE`")
77+
raise ValueError("messageId is available only when the `eventType` is `MESSAGE`")
7878
return values
7979

80+
@validator("requestTimeEpoch", pre=True)
81+
def normalize_timestamp(cls, value):
82+
date_utc = datetime.fromtimestamp(int(value) / 1000, tz=timezone.utc)
83+
return date_utc
84+
8085

8186
class APIGatewayProxyEventModel(BaseModel):
82-
version: Optional[str]
87+
version: Optional[str] = None
8388
resource: str
8489
path: str
8590
httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
8691
headers: Dict[str, str]
8792
multiValueHeaders: Dict[str, List[str]]
88-
queryStringParameters: Optional[Dict[str, str]]
89-
multiValueQueryStringParameters: Optional[Dict[str, List[str]]]
93+
queryStringParameters: Optional[Dict[str, str]] = None
94+
multiValueQueryStringParameters: Optional[Dict[str, List[str]]] = None
9095
requestContext: APIGatewayEventRequestContext
91-
pathParameters: Optional[Dict[str, str]]
92-
stageVariables: Optional[Dict[str, str]]
96+
pathParameters: Optional[Dict[str, str]] = None
97+
stageVariables: Optional[Dict[str, str]] = None
9398
isBase64Encoded: bool
94-
body: Optional[Union[str, Type[BaseModel]]]
99+
body: Optional[Union[str, Type[BaseModel]]] = None
Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from datetime import datetime
1+
from datetime import datetime, timezone
22
from typing import Any, Dict, List, Optional, Type, Union
33

4-
from pydantic import BaseModel, Field
4+
from pydantic import BaseModel, Field, validator
55
from pydantic.networks import IPvAnyNetwork
66

77
from aws_lambda_powertools.utilities.parser.types import Literal
@@ -14,13 +14,13 @@ class RequestContextV2AuthorizerIamCognito(BaseModel):
1414

1515

1616
class RequestContextV2AuthorizerIam(BaseModel):
17-
accessKey: Optional[str]
18-
accountId: Optional[str]
19-
callerId: Optional[str]
20-
principalOrgId: Optional[str]
21-
userArn: Optional[str]
22-
userId: Optional[str]
23-
cognitoIdentity: Optional[RequestContextV2AuthorizerIamCognito]
17+
accessKey: Optional[str] = None
18+
accountId: Optional[str] = None
19+
callerId: Optional[str] = None
20+
principalOrgId: Optional[str] = None
21+
userArn: Optional[str] = None
22+
userId: Optional[str] = None
23+
cognitoIdentity: Optional[RequestContextV2AuthorizerIamCognito] = None
2424

2525

2626
class RequestContextV2AuthorizerJwt(BaseModel):
@@ -29,9 +29,9 @@ class RequestContextV2AuthorizerJwt(BaseModel):
2929

3030

3131
class RequestContextV2Authorizer(BaseModel):
32-
jwt: Optional[RequestContextV2AuthorizerJwt]
33-
iam: Optional[RequestContextV2AuthorizerIam]
34-
lambda_value: Optional[Dict[str, Any]] = Field(None, alias="lambda")
32+
jwt: Optional[RequestContextV2AuthorizerJwt] = None
33+
iam: Optional[RequestContextV2AuthorizerIam] = None
34+
lambda_value: Union[Dict[str, Any], None] = Field(None, alias="lambda")
3535

3636

3737
class RequestContextV2Http(BaseModel):
@@ -45,7 +45,7 @@ class RequestContextV2Http(BaseModel):
4545
class RequestContextV2(BaseModel):
4646
accountId: str
4747
apiId: str
48-
authorizer: Optional[RequestContextV2Authorizer]
48+
authorizer: Optional[RequestContextV2Authorizer] = None
4949
domainName: str
5050
domainPrefix: str
5151
requestId: str
@@ -55,17 +55,22 @@ class RequestContextV2(BaseModel):
5555
timeEpoch: datetime
5656
http: RequestContextV2Http
5757

58+
@validator("timeEpoch", pre=True)
59+
def normalize_timestamp(cls, value):
60+
date_utc = datetime.fromtimestamp(int(value) / 1000, tz=timezone.utc)
61+
return date_utc
62+
5863

5964
class APIGatewayProxyEventV2Model(BaseModel):
6065
version: str
6166
routeKey: str
6267
rawPath: str
6368
rawQueryString: str
64-
cookies: Optional[List[str]]
69+
cookies: Optional[List[str]] = None
6570
headers: Dict[str, str]
66-
queryStringParameters: Optional[Dict[str, str]]
67-
pathParameters: Optional[Dict[str, str]]
68-
stageVariables: Optional[Dict[str, str]]
71+
queryStringParameters: Optional[Dict[str, str]] = None
72+
pathParameters: Optional[Dict[str, str]] = None
73+
stageVariables: Optional[Dict[str, str]] = None
6974
requestContext: RequestContextV2
70-
body: Optional[Union[str, Type[BaseModel]]]
75+
body: Optional[Union[str, Type[BaseModel]]] = None
7176
isBase64Encoded: bool

aws_lambda_powertools/utilities/parser/models/cloudwatch.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json
33
import logging
44
import zlib
5-
from datetime import datetime
5+
from datetime import datetime, timezone
66
from typing import List, Type, Union
77

88
from pydantic import BaseModel, Field, validator
@@ -15,6 +15,11 @@ class CloudWatchLogsLogEvent(BaseModel):
1515
timestamp: datetime
1616
message: Union[str, Type[BaseModel]]
1717

18+
@validator("timestamp", pre=True)
19+
def coerc_timestamp(cls, value):
20+
date_utc = datetime.fromtimestamp(value / 1000, tz=timezone.utc)
21+
return date_utc
22+
1823

1924
class CloudWatchLogsDecode(BaseModel):
2025
messageType: str

aws_lambda_powertools/utilities/parser/models/dynamodb.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77

88

99
class DynamoDBStreamChangedRecordModel(BaseModel):
10-
ApproximateCreationDateTime: Optional[date]
10+
ApproximateCreationDateTime: Optional[date] = None
1111
Keys: Dict[str, Dict[str, Any]]
12-
NewImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]]
13-
OldImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]]
12+
NewImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] = None
13+
OldImage: Optional[Union[Dict[str, Any], Type[BaseModel], BaseModel]] = None
1414
SequenceNumber: str
1515
SizeBytes: int
1616
StreamViewType: Literal["NEW_AND_OLD_IMAGES", "KEYS_ONLY", "NEW_IMAGE", "OLD_IMAGE"]
@@ -40,7 +40,7 @@ class DynamoDBStreamRecordModel(BaseModel):
4040
awsRegion: str
4141
eventSourceARN: str
4242
dynamodb: DynamoDBStreamChangedRecordModel
43-
userIdentity: Optional[UserIdentity]
43+
userIdentity: Optional[UserIdentity] = None
4444

4545

4646
class DynamoDBStreamModel(BaseModel):

aws_lambda_powertools/utilities/parser/models/event_bridge.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime
2-
from typing import List, Optional
2+
from typing import List, Union
33

44
from pydantic import BaseModel, Field
55

@@ -16,4 +16,4 @@ class EventBridgeModel(BaseModel):
1616
resources: List[str]
1717
detail_type: str = Field(None, alias="detail-type")
1818
detail: RawDictOrModel
19-
replay_name: Optional[str] = Field(None, alias="replay-name")
19+
replay_name: Union[str, None] = Field(None, alias="replay-name")

0 commit comments

Comments
 (0)