Skip to content

feat(parser): add support for Pydantic v2 #2733

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8daf90c
pydantic v2: initial tests
leandrodamascena Jul 8, 2023
de53dd6
pydantic v2: comment
leandrodamascena Jul 8, 2023
6f8c52b
pydantic v2: new workflow
leandrodamascena Jul 8, 2023
82b166c
pydantic v2: comment
leandrodamascena Jul 8, 2023
7295798
pydantic v2: mypy fix
leandrodamascena Jul 8, 2023
201d877
pydantic v2: fix v2 compability
leandrodamascena Jul 8, 2023
bf6b31a
pydantic v2: fix last things
leandrodamascena Jul 10, 2023
ef98e88
pydantic v2: improving comments
leandrodamascena Jul 10, 2023
f39ea89
pydantic v2: addressing Heitor's feedback
leandrodamascena Jul 10, 2023
15fab06
pydantic v2: creating pydantic v2 specific test
leandrodamascena Jul 10, 2023
e5d6318
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 10, 2023
b0f5fb3
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 10, 2023
6f30f08
pydantic v2: using fixture to clean the code
leandrodamascena Jul 11, 2023
cec6630
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 12, 2023
3d5c9b2
pydanticv2: reverting Optional fields
leandrodamascena Jul 12, 2023
3ee041b
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 12, 2023
7279137
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 13, 2023
07e483d
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 13, 2023
e6abd65
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 17, 2023
ce15df0
Removing the validators. Pydantic bug was fixed
Jul 17, 2023
d4f8171
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 17, 2023
f73a222
Adding pytest ignore messages for Pydantic v2
Jul 17, 2023
1774a1c
Adding pytest ignore messages for Pydantic v2
Jul 17, 2023
e7c1c34
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 18, 2023
f1bb815
Merge branch 'develop' into poc/pydanticv2
leandrodamascena Jul 20, 2023
3a5d26f
pydanticv2: removing duplicated workflow + disabling warning
leandrodamascena Jul 20, 2023
53e4e98
pydanticv2: adding documentation
leandrodamascena Jul 21, 2023
49561b2
Adding cache to disable pydantic warnings
Jul 21, 2023
eef0dc1
Adjusting workflow
Jul 21, 2023
f8470f5
Addressing Heitor's feedback
Jul 21, 2023
fa298d2
Removed codecov upload
Jul 21, 2023
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
78 changes: 78 additions & 0 deletions .github/workflows/quality_check_temp_pydanticv1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Code quality temp - Pydanticv1

# 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

# USAGE
#
# Always triggered on new PRs, PR changes and PR merge.


on:
pull_request:
paths:
- "aws_lambda_powertools/**"
- "tests/**"
- "pyproject.toml"
- "poetry.lock"
- "mypy.ini"
branches:
- poc/pydanticv2
push:
paths:
- "aws_lambda_powertools/**"
- "tests/**"
- "pyproject.toml"
- "poetry.lock"
- "mypy.ini"
branches:
- poc/pydanticv2

permissions:
contents: read

jobs:
quality_check:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
env:
PYTHON: "${{ matrix.python-version }}"
permissions:
contents: read # checkout code only
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Install poetry
run: pipx install poetry
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
run: make dev
- name: Formatting and Linting
run: make lint
- name: Static type checking
run: make mypy
- name: Test with pytest
run: make test
- name: Security baseline
run: make security-baseline
- name: Complexity baseline
run: make complexity-baseline
- name: Upload coverage to Codecov
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # 3.1.4
with:
file: ./coverage.xml
env_vars: PYTHON
name: aws-lambda-powertools-python-codecov
82 changes: 82 additions & 0 deletions .github/workflows/quality_check_temp_pydanticv2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Code quality temp - Pydanticv2

# 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

# USAGE
#
# Always triggered on new PRs, PR changes and PR merge.


on:
pull_request:
paths:
- "aws_lambda_powertools/**"
- "tests/**"
- "pyproject.toml"
- "poetry.lock"
- "mypy.ini"
branches:
- poc/pydanticv2
push:
paths:
- "aws_lambda_powertools/**"
- "tests/**"
- "pyproject.toml"
- "poetry.lock"
- "mypy.ini"
branches:
- poc/pydanticv2

permissions:
contents: read

jobs:
quality_check:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
env:
PYTHON: "${{ matrix.python-version }}"
permissions:
contents: read # checkout code only
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Install poetry
run: pipx install poetry
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Removing dev dependencies locked to Pydantic v1
run: poetry remove cfn-lint
- name: Replacing Pydantic v1 with v2
run: poetry add "pydantic>=2.0"
- name: Install dependencies
run: make dev
- name: Formatting and Linting
run: make lint
- name: Static type checking
run: make mypy
- name: Test with pytest
run: make test
- name: Security baseline
run: make security-baseline
- name: Complexity baseline
run: make complexity-baseline
- name: Upload coverage to Codecov
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # 3.1.4
with:
file: ./coverage.xml
env_vars: PYTHON
name: aws-lambda-powertools-python-codecov
6 changes: 3 additions & 3 deletions aws_lambda_powertools/shared/user_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def register_feature_to_session(session, feature):
def register_feature_to_botocore_session(botocore_session, feature):
"""
Register the given feature string to the event system of the provided botocore session

Please notice this function is for patching botocore session and is different from
previous one which is for patching boto3 session

Expand All @@ -127,7 +127,7 @@ def register_feature_to_botocore_session(botocore_session, feature):
------
AttributeError
If the provided session does not have an event system.

Examples
--------
**register data-masking user-agent to botocore session**
Expand All @@ -139,7 +139,7 @@ def register_feature_to_botocore_session(botocore_session, feature):
>>> session = botocore.session.Session()
>>> register_feature_to_botocore_session(botocore_session=session, feature="data-masking")
>>> key_provider = StrictAwsKmsMasterKeyProvider(key_ids=self.keys, botocore_session=session)

"""
try:
botocore_session.register(TARGET_SDK_EVENT, _create_feature_function(feature))
Expand Down
23 changes: 19 additions & 4 deletions aws_lambda_powertools/utilities/batch/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,11 @@ def _to_batch_type(self, record: dict, event_type: EventType) -> EventSourceData

def _to_batch_type(self, record: dict, event_type: EventType, model: Optional["BatchTypeModels"] = None):
if model is not None:
# If a model is provided, we assume Pydantic is installed and we need to disable v2 warnings
from aws_lambda_powertools.utilities.parser.compat import disable_pydantic_v2_warning

disable_pydantic_v2_warning()

return model.parse_obj(record)
return self._DATA_CLASS_MAPPING[event_type](record)

Expand Down Expand Up @@ -500,8 +505,13 @@ def _process_record(self, record: dict) -> Union[SuccessResponse, FailureRespons
# we need to handle that exception differently.
# We check for a public attr in validation errors coming from Pydantic exceptions (subclass or not)
# and we compare if it's coming from the same model that trigger the exception in the first place
model = getattr(exc, "model", None)
if model == self.model:

# Pydantic v1 raises a ValidationError with ErrorWrappers and store the model instance in a class variable.
# Pydantic v2 simplifies this by adding a title variable to store the model name directly.
model = getattr(exc, "model", None) or getattr(exc, "title", None)
model_name = getattr(self.model, "__name__", None)

if model == self.model or model == model_name:
return self._register_model_validation_error_record(record)

return self.failure_handler(record=data, exception=sys.exc_info())
Expand Down Expand Up @@ -644,8 +654,13 @@ async def _async_process_record(self, record: dict) -> Union[SuccessResponse, Fa
# we need to handle that exception differently.
# We check for a public attr in validation errors coming from Pydantic exceptions (subclass or not)
# and we compare if it's coming from the same model that trigger the exception in the first place
model = getattr(exc, "model", None)
if model == self.model:

# Pydantic v1 raises a ValidationError with ErrorWrappers and store the model instance in a class variable.
# Pydantic v2 simplifies this by adding a title variable to store the model name directly.
model = getattr(exc, "model", None) or getattr(exc, "title", None)
model_name = getattr(self.model, "__name__", None)

if model == self.model or model == model_name:
return self._register_model_validation_error_record(record)

return self.failure_handler(record=data, exception=sys.exc_info())
28 changes: 28 additions & 0 deletions aws_lambda_powertools/utilities/parser/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
def disable_pydantic_v2_warning():
"""
Disables the Pydantic version 2 warning by filtering out the related warnings.

This function checks the version of Pydantic currently installed and if it is version 2,
it filters out the PydanticDeprecationWarning and PydanticDeprecatedSince20 warnings
to suppress them.

Note: This function assumes that Pydantic is already imported.

Usage:
disable_pydantic_v2_warning()
"""
try:
from pydantic import __version__

version = __version__.split(".")

if int(version[0]) == 2:
import warnings

from pydantic import PydanticDeprecatedSince20, PydanticDeprecationWarning

warnings.filterwarnings("ignore", category=PydanticDeprecationWarning)
warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20)

except ImportError:
pass
4 changes: 4 additions & 0 deletions aws_lambda_powertools/utilities/parser/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from aws_lambda_powertools.utilities.parser.compat import disable_pydantic_v2_warning

disable_pydantic_v2_warning()

from .alb import AlbModel, AlbRequestContext, AlbRequestContextData
from .apigw import (
APIGatewayEventAuthorizer,
Expand Down
86 changes: 47 additions & 39 deletions aws_lambda_powertools/utilities/parser/models/apigw.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Type, Union

from pydantic import BaseModel, root_validator
from pydantic import BaseModel, root_validator, validator
from pydantic.networks import IPvAnyNetwork

from aws_lambda_powertools.utilities.parser.types import Literal
Expand All @@ -21,74 +21,82 @@ class ApiGatewayUserCert(BaseModel):


class APIGatewayEventIdentity(BaseModel):
accessKey: Optional[str]
accountId: Optional[str]
apiKey: Optional[str]
apiKeyId: Optional[str]
caller: Optional[str]
cognitoAuthenticationProvider: Optional[str]
cognitoAuthenticationType: Optional[str]
cognitoIdentityId: Optional[str]
cognitoIdentityPoolId: Optional[str]
principalOrgId: Optional[str]
accessKey: Optional[str] = None
accountId: Optional[str] = None
apiKey: Optional[str] = None
apiKeyId: Optional[str] = None
caller: Optional[str] = None
cognitoAuthenticationProvider: Optional[str] = None
cognitoAuthenticationType: Optional[str] = None
cognitoIdentityId: Optional[str] = None
cognitoIdentityPoolId: Optional[str] = None
principalOrgId: Optional[str] = None
# see #1562, temp workaround until API Gateway fixes it the Test button payload
# removing it will not be considered a regression in the future
sourceIp: Union[IPvAnyNetwork, Literal["test-invoke-source-ip"]]
user: Optional[str]
userAgent: Optional[str]
userArn: Optional[str]
clientCert: Optional[ApiGatewayUserCert]
user: Optional[str] = None
userAgent: Optional[str] = None
userArn: Optional[str] = None
clientCert: Optional[ApiGatewayUserCert] = None


class APIGatewayEventAuthorizer(BaseModel):
claims: Optional[Dict[str, Any]]
scopes: Optional[List[str]]
claims: Optional[Dict[str, Any]] = None
scopes: Optional[List[str]] = None


class APIGatewayEventRequestContext(BaseModel):
accountId: str
apiId: str
authorizer: Optional[APIGatewayEventAuthorizer]
authorizer: Optional[APIGatewayEventAuthorizer] = None
stage: str
protocol: str
identity: APIGatewayEventIdentity
requestId: str
requestTime: str
requestTimeEpoch: datetime
resourceId: Optional[str]
resourceId: Optional[str] = None
resourcePath: str
domainName: Optional[str]
domainPrefix: Optional[str]
extendedRequestId: Optional[str]
domainName: Optional[str] = None
domainPrefix: Optional[str] = None
extendedRequestId: Optional[str] = None
httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
path: str
connectedAt: Optional[datetime]
connectionId: Optional[str]
eventType: Optional[Literal["CONNECT", "MESSAGE", "DISCONNECT"]]
messageDirection: Optional[str]
messageId: Optional[str]
routeKey: Optional[str]
operationName: Optional[str]

@root_validator(allow_reuse=True)
connectedAt: Optional[datetime] = None
connectionId: Optional[str] = None
eventType: Optional[Literal["CONNECT", "MESSAGE", "DISCONNECT"]] = None
messageDirection: Optional[str] = None
messageId: Optional[str] = None
routeKey: Optional[str] = None
operationName: Optional[str] = None

@root_validator(allow_reuse=True, skip_on_failure=True)
def check_message_id(cls, values):
message_id, event_type = values.get("messageId"), values.get("eventType")
if message_id is not None and event_type != "MESSAGE":
raise TypeError("messageId is available only when the `eventType` is `MESSAGE`")
raise ValueError("messageId is available only when the `eventType` is `MESSAGE`")
return values

# Validator to normalize the requestTimeEpoch field
# Converts the provided timestamp value to a UTC datetime object
# See: https://github.com/pydantic/pydantic/issues/6518
@validator("requestTimeEpoch", pre=True)
def coerce_timestamp(cls, value):
date_utc = datetime.fromtimestamp(int(value) / 1000, tz=timezone.utc)
return date_utc


class APIGatewayProxyEventModel(BaseModel):
version: Optional[str]
version: Optional[str] = None
resource: str
path: str
httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
headers: Dict[str, str]
multiValueHeaders: Dict[str, List[str]]
queryStringParameters: Optional[Dict[str, str]]
multiValueQueryStringParameters: Optional[Dict[str, List[str]]]
queryStringParameters: Optional[Dict[str, str]] = None
multiValueQueryStringParameters: Optional[Dict[str, List[str]]] = None
requestContext: APIGatewayEventRequestContext
pathParameters: Optional[Dict[str, str]]
stageVariables: Optional[Dict[str, str]]
pathParameters: Optional[Dict[str, str]] = None
stageVariables: Optional[Dict[str, str]] = None
isBase64Encoded: bool
body: Optional[Union[str, Type[BaseModel]]]
body: Optional[Union[str, Type[BaseModel]]] = None
Loading