From e3c84d532369a796b6903cecb01bebeb5ef45843 Mon Sep 17 00:00:00 2001 From: NaqGuug Date: Sun, 3 May 2026 17:28:04 +0300 Subject: [PATCH 1/2] feat: Forbid bulkId field --- scim2_models/base.py | 22 ++++++++++++++++++++++ scim2_models/messages/bulk.py | 2 ++ tests/test_model_attributes.py | 12 ++++++++++++ 3 files changed, 36 insertions(+) diff --git a/scim2_models/base.py b/scim2_models/base.py index d427673..7b98235 100644 --- a/scim2_models/base.py +++ b/scim2_models/base.py @@ -2,6 +2,7 @@ from inspect import isclass from typing import Any from typing import Optional +from typing import ClassVar from typing import get_args from typing import get_origin @@ -112,6 +113,14 @@ class BaseModel(PydanticBaseModel): extra="forbid", ) + _allow_bulk_id: ClassVar[bool] = False + """Allow bulkId field for the model""" + + @classmethod + def __pydantic_init_subclass__(cls, **kwargs) -> None: + # Validate field names + cls._check_bulk_id() + @classmethod def get_field_annotation(cls, field_name: str, annotation_type: type) -> Any: """Return the annotation of type 'annotation_type' of the field 'field_name'. @@ -538,6 +547,19 @@ def _set_complex_attribute_urns(self) -> None: else: attr_value._attribute_urn = schema + @classmethod + def _check_bulk_id(cls) -> None: + """Enforce bulkId as reserved field per RFC 7643 ยง3.1. + + Check if a bulkdId field is defined and + raise error if `_allow_bulk_id` is set to `False` + """ + if cls._allow_bulk_id: + return + for info in cls.model_fields.values(): + if info.serialization_alias == "bulkId": + raise TypeError(f"{cls.__name__}: bulkId is reserved for BulkOperation") + @field_serializer("*", mode="wrap") def scim_serializer( self, diff --git a/scim2_models/messages/bulk.py b/scim2_models/messages/bulk.py index 7c2f19f..1468ba4 100644 --- a/scim2_models/messages/bulk.py +++ b/scim2_models/messages/bulk.py @@ -12,6 +12,8 @@ class BulkOperation(ComplexAttribute): + _allow_bulk_id = True + class Method(str, Enum): post = "POST" put = "PUT" diff --git a/tests/test_model_attributes.py b/tests/test_model_attributes.py index 418b88c..fbf4da3 100644 --- a/tests/test_model_attributes.py +++ b/tests/test_model_attributes.py @@ -1,5 +1,7 @@ import uuid +import pytest from typing import Annotated +from pydantic import Field from scim2_models import URN from scim2_models.annotations import Returned @@ -377,3 +379,13 @@ def test_short_attr_path_with_plain_name(): assert _short_attr_path("userName") == "userName" assert _short_attr_path("name.familyName") == "name.familyName" + + +def test_forbid_bulk_id(): + """Forbid bulkId from class definition""" + with pytest.raises(TypeError) as exc_info: + class CustomModel(Resource): + __schema__ = URN("urn:example:schemas:CustomModel") + bulk_id: str | None = None + + assert str(exc_info.value) == "CustomModel: bulkId is reserved for BulkOperation" From 6fd5b58e801c79e88c61dfc6bf0352d40525088f Mon Sep 17 00:00:00 2001 From: NaqGuug Date: Sun, 3 May 2026 17:45:34 +0300 Subject: [PATCH 2/2] fix: Pass linting and type checking --- scim2_models/base.py | 4 ++-- tests/test_model_attributes.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/scim2_models/base.py b/scim2_models/base.py index 7b98235..396bd47 100644 --- a/scim2_models/base.py +++ b/scim2_models/base.py @@ -1,8 +1,8 @@ import warnings from inspect import isclass from typing import Any -from typing import Optional from typing import ClassVar +from typing import Optional from typing import get_args from typing import get_origin @@ -117,7 +117,7 @@ class BaseModel(PydanticBaseModel): """Allow bulkId field for the model""" @classmethod - def __pydantic_init_subclass__(cls, **kwargs) -> None: + def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: # Validate field names cls._check_bulk_id() diff --git a/tests/test_model_attributes.py b/tests/test_model_attributes.py index fbf4da3..428bfa9 100644 --- a/tests/test_model_attributes.py +++ b/tests/test_model_attributes.py @@ -1,7 +1,7 @@ import uuid -import pytest from typing import Annotated -from pydantic import Field + +import pytest from scim2_models import URN from scim2_models.annotations import Returned @@ -382,8 +382,9 @@ def test_short_attr_path_with_plain_name(): def test_forbid_bulk_id(): - """Forbid bulkId from class definition""" + """Forbid bulkId from class definition.""" with pytest.raises(TypeError) as exc_info: + class CustomModel(Resource): __schema__ = URN("urn:example:schemas:CustomModel") bulk_id: str | None = None