From 34f3b37ec8a8156967954b09b6e1cfc881e6c5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Mon, 2 Dec 2024 16:51:03 +0100 Subject: [PATCH] fix: Base64Bytes compatibility with all supported pydantic versions The behavior changed with Pydantic 2.10, which broke some unit tests. https://pydantic.dev/articles/pydantic-v2-10-release#use-b64decode-and-b64encode-for-base64bytes-and-base64str-types --- doc/changelog.rst | 11 +++++++ scim2_models/rfc7643/schema.py | 2 +- scim2_models/rfc7643/user.py | 2 +- scim2_models/utils.py | 57 +++++++++++++++++++++++++++++++++ tests/test_dynamic_resources.py | 3 +- 5 files changed, 71 insertions(+), 4 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 15b4199..012b76e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,17 @@ Changelog ========= +[0.2.10] - 2024-12-02 +--------------------- + +Changed +^^^^^^^ +- The ``schema`` attribute is annotated with :attr:`~scim2_models.Required.true`. + +Fixed +^^^^^ +- ``Base64Bytes`` compatibility between pydantic 2.10+ and <2.10 + [0.2.9] - 2024-12-02 -------------------- diff --git a/scim2_models/rfc7643/schema.py b/scim2_models/rfc7643/schema.py index 7fcad8a..d236645 100644 --- a/scim2_models/rfc7643/schema.py +++ b/scim2_models/rfc7643/schema.py @@ -9,7 +9,6 @@ from typing import Union from typing import get_origin -from pydantic import Base64Bytes from pydantic import Field from pydantic import create_model from pydantic import field_validator @@ -30,6 +29,7 @@ from ..base import URIReference from ..base import is_complex_attribute from ..constants import RESERVED_WORDS +from ..utils import Base64Bytes from ..utils import normalize_attribute_name from .resource import Extension from .resource import Resource diff --git a/scim2_models/rfc7643/user.py b/scim2_models/rfc7643/user.py index 1c729e5..d1d2239 100644 --- a/scim2_models/rfc7643/user.py +++ b/scim2_models/rfc7643/user.py @@ -4,7 +4,6 @@ from typing import Optional from typing import Union -from pydantic import Base64Bytes from pydantic import EmailStr from pydantic import Field @@ -17,6 +16,7 @@ from ..base import Required from ..base import Returned from ..base import Uniqueness +from ..utils import Base64Bytes from .resource import Resource diff --git a/scim2_models/utils.py b/scim2_models/utils.py index bdae9e7..766376f 100644 --- a/scim2_models/utils.py +++ b/scim2_models/utils.py @@ -1,8 +1,14 @@ +import base64 import re +from typing import Annotated +from typing import Literal from typing import Optional from typing import Union +from pydantic import EncodedBytes +from pydantic import EncoderProtocol from pydantic.alias_generators import to_snake +from pydantic_core import PydanticCustomError try: from types import UnionType # type: ignore @@ -17,6 +23,57 @@ def int_to_str(status: Optional[int]) -> Optional[str]: return None if status is None else str(status) +# Copied from Pydantic 2.10 repository +class Base64Encoder(EncoderProtocol): # pragma: no cover + """Standard (non-URL-safe) Base64 encoder.""" + + @classmethod + def decode(cls, data: bytes) -> bytes: + """Decode the data from base64 encoded bytes to original bytes data. + + Args: + data: The data to decode. + + Returns: + The decoded data. + + """ + try: + return base64.b64decode(data) + except ValueError as e: + raise PydanticCustomError( + "base64_decode", "Base64 decoding error: '{error}'", {"error": str(e)} + ) from e + + @classmethod + def encode(cls, value: bytes) -> bytes: + """Encode the data from bytes to a base64 encoded bytes. + + Args: + value: The data to encode. + + Returns: + The encoded data. + + """ + return base64.b64encode(value) + + @classmethod + def get_json_format(cls) -> Literal["base64"]: + """Get the JSON format for the encoded data. + + Returns: + The JSON format for the encoded data. + + """ + return "base64" + + +# Compatibility with Pydantic <2.10 +# https://pydantic.dev/articles/pydantic-v2-10-release#use-b64decode-and-b64encode-for-base64bytes-and-base64str-types +Base64Bytes = Annotated[bytes, EncodedBytes(encoder=Base64Encoder)] + + def to_camel(string: str) -> str: """Transform strings to camelCase. diff --git a/tests/test_dynamic_resources.py b/tests/test_dynamic_resources.py index c817f06..4e0e24c 100644 --- a/tests/test_dynamic_resources.py +++ b/tests/test_dynamic_resources.py @@ -2,8 +2,6 @@ from typing import Literal from typing import Union -from pydantic import Base64Bytes - from scim2_models.base import CaseExact from scim2_models.base import ComplexAttribute from scim2_models.base import ExternalReference @@ -18,6 +16,7 @@ from scim2_models.rfc7643.resource import Resource from scim2_models.rfc7643.schema import Attribute from scim2_models.rfc7643.schema import Schema +from scim2_models.utils import Base64Bytes def test_make_group_model_from_schema(load_sample):