Skip to content

Commit cda0e72

Browse files
committed
fix: multi-value attributes patch
1 parent ae0a7ee commit cda0e72

File tree

2 files changed

+62
-34
lines changed

2 files changed

+62
-34
lines changed

scim2_server/utils.py

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,42 @@ def handle_extension(resource: Resource, scim_name: str) -> tuple[BaseModel, str
144144
return resource, scim_name
145145

146146

147-
def model_validate_from_dict(field_root_type: type[BaseModel], value: dict) -> Any:
148-
"""Workaround for some of the "special" requirements for MS Entra, mixing display and displayName in some cases."""
149-
if (
150-
"display" not in value
151-
and "display" in field_root_type.model_fields
152-
and "displayName" in value
153-
):
154-
value["display"] = value["displayName"]
155-
del value["displayName"]
156-
return field_root_type.model_validate(value)
147+
def parse_value(field_root_type: type, value: Any) -> Any:
148+
"""Parse a PATCH value according to the target field root type."""
149+
if isinstance(value, dict):
150+
if not hasattr(field_root_type, "model_fields"):
151+
raise TypeError
152+
153+
# Work around mixed display/displayName payloads emitted by MS Entra.
154+
if (
155+
"display" not in value
156+
and "display" in field_root_type.model_fields
157+
and "displayName" in value
158+
):
159+
value = value.copy()
160+
value["display"] = value["displayName"]
161+
del value["displayName"]
162+
return field_root_type.model_validate(value)
163+
164+
if field_root_type is bool and isinstance(value, str):
165+
return not value.lower() == "false"
166+
167+
if field_root_type is datetime.datetime and isinstance(value, str):
168+
# ISO 8601 datetime format (notably with the Z suffix) are only supported from Python 3.11
169+
if sys.version_info < (3, 11): # pragma: no cover
170+
return datetime.datetime.fromisoformat(re.sub(r"Z$", "+00:00", value))
171+
return datetime.datetime.fromisoformat(value)
172+
173+
if field_root_type is EmailStr and isinstance(value, str):
174+
return value
175+
176+
if hasattr(field_root_type, "model_fields"):
177+
primary_value = get_by_alias(field_root_type, "value", True)
178+
if primary_value is not None:
179+
return field_root_type(value=value)
180+
raise TypeError
181+
182+
return field_root_type(value)
157183

158184

159185
def parse_new_value(model: BaseModel, attribute_name: str, value: Any) -> Any:
@@ -164,31 +190,10 @@ def parse_new_value(model: BaseModel, attribute_name: str, value: Any) -> Any:
164190
"""
165191
field_root_type = model.get_field_root_type(attribute_name)
166192
try:
167-
if isinstance(value, dict):
168-
new_value = model_validate_from_dict(field_root_type, value)
169-
elif isinstance(value, list):
170-
new_value = [model_validate_from_dict(field_root_type, v) for v in value]
193+
if isinstance(value, list):
194+
new_value = [parse_value(field_root_type, v) for v in value]
171195
else:
172-
if field_root_type is bool and isinstance(value, str):
173-
new_value = not value.lower() == "false"
174-
elif field_root_type is datetime.datetime and isinstance(value, str):
175-
# ISO 8601 datetime format (notably with the Z suffix) are only supported from Python 3.11
176-
if sys.version_info < (3, 11): # pragma: no cover
177-
new_value = datetime.datetime.fromisoformat(
178-
re.sub(r"Z$", "+00:00", value)
179-
)
180-
else:
181-
new_value = datetime.datetime.fromisoformat(value)
182-
elif field_root_type is EmailStr and isinstance(value, str):
183-
new_value = value
184-
elif hasattr(field_root_type, "model_fields"):
185-
primary_value = get_by_alias(field_root_type, "value", True)
186-
if primary_value is not None:
187-
new_value = field_root_type(value=value)
188-
else:
189-
raise TypeError
190-
else:
191-
new_value = field_root_type(value)
196+
new_value = parse_value(field_root_type, value)
192197
except (AttributeError, TypeError, ValueError, ValidationError) as e:
193198
raise InvalidValueException() from e
194199
return new_value

tests/test_patch.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
2+
from scim2_models import URN
23
from scim2_models import MutabilityException
34
from scim2_models import PatchOperation
5+
from scim2_models.resources.resource import Resource
46

57
from scim2_server.operators import patch_resource
68

@@ -200,3 +202,24 @@ def test_patch_operation_add_multi_valued(self, provider):
200202
},
201203
],
202204
}
205+
206+
def test_patch_replace_multivalued_primitive_attribute(self):
207+
"""Replace a multi-valued primitive attribute."""
208+
209+
class MyResource(Resource):
210+
__schema__ = URN("urn:example:schemas:MyResource")
211+
212+
tags: list[str] | None = None
213+
214+
resource = MyResource(id="123")
215+
216+
patch_resource(
217+
resource,
218+
PatchOperation(
219+
op=PatchOperation.Op.replace_,
220+
path="tags",
221+
value=["tag1", "tag2"],
222+
),
223+
)
224+
225+
assert resource.tags == ["tag1", "tag2"]

0 commit comments

Comments
 (0)