Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Fixed
^^^^^
- Fix ``model_json_schema()`` generation for models containing :class:`~scim2_models.Reference` or :class:`~scim2_models.Path` fields. :issue:`125`
- Group ``displayName`` is required. :rfc:`7643` `erratum 5368 <https://www.rfc-editor.org/errata/eid5368>`_ :issue:`123` :pr:`128`
- :class:`~scim2_models.GroupMembership` ``$ref`` only references ``Group``. :rfc:`7643` `erratum 8471 <https://www.rfc-editor.org/errata/eid8471>`_
- :class:`~scim2_models.Manager` ``value`` is case-exact. :rfc:`7643` `erratum 8472 <https://www.rfc-editor.org/errata/eid8472>`_
- :class:`~scim2_models.ResourceType` ``name`` and ``endpoint`` have server uniqueness. :rfc:`7643` `erratum 8475 <https://www.rfc-editor.org/errata/eid8475>`_
- Complex attributes don't have ``uniqueness`` in schema representation. :rfc:`7643` `erratum 6004 <https://www.rfc-editor.org/errata/eid6004>`_

[0.6.2] - 2026-01-25
--------------------
Expand Down
2 changes: 1 addition & 1 deletion samples/rfc7643-8.7.1-schema-enterprise_user.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"multiValued": false,
"description": "The id of the SCIM resource representing the User's manager. REQUIRED.",
"required": true,
"caseExact": false,
"caseExact": true,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
Expand Down
1,552 changes: 775 additions & 777 deletions samples/rfc7643-8.7.1-schema-user.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions samples/rfc7643-8.7.2-schema-resource_type.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
"multiValued": false,
"description": "The resource type name. When applicable, service providers MUST specify the name, e.g., 'User'.",
"required": true,
"caseExact": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
"uniqueness": "server"
},
{
"name": "description",
Expand All @@ -49,7 +49,7 @@
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
"uniqueness": "server"
},
{
"name": "schema",
Expand Down
3 changes: 2 additions & 1 deletion scim2_models/resources/enterprise_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from pydantic import Field

from ..annotations import CaseExact
from ..annotations import Mutability
from ..annotations import Required
from ..attributes import ComplexAttribute
Expand All @@ -15,7 +16,7 @@


class Manager(ComplexAttribute):
value: Annotated[str | None, Required.true] = None
value: Annotated[str | None, Required.true, CaseExact.true] = None
"""The id of the SCIM resource representing the User's manager."""

ref: Annotated[ # type: ignore[type-arg]
Expand Down
34 changes: 18 additions & 16 deletions scim2_models/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,19 +532,21 @@ def _model_attribute_to_scim_attribute(
else None
)

return Attribute(
name=field_info.serialization_alias or attribute_name,
type=Attribute.Type(attribute_type),
multi_valued=model.get_field_multiplicity(attribute_name),
description=field_info.description,
canonical_values=field_info.examples,
required=model.get_field_annotation(attribute_name, Required),
case_exact=model.get_field_annotation(attribute_name, CaseExact),
mutability=model.get_field_annotation(attribute_name, Mutability),
returned=model.get_field_annotation(attribute_name, Returned),
uniqueness=model.get_field_annotation(attribute_name, Uniqueness),
sub_attributes=sub_attributes,
reference_types=root_type.get_scim_reference_types() # type: ignore[attr-defined]
if attribute_type == Attribute.Type.reference
else None,
)
kwargs: dict[str, Any] = {
"name": field_info.serialization_alias or attribute_name,
"type": Attribute.Type(attribute_type),
"multi_valued": model.get_field_multiplicity(attribute_name),
"description": field_info.description,
"canonical_values": field_info.examples,
"required": model.get_field_annotation(attribute_name, Required),
"case_exact": model.get_field_annotation(attribute_name, CaseExact),
"mutability": model.get_field_annotation(attribute_name, Mutability),
"returned": model.get_field_annotation(attribute_name, Returned),
"sub_attributes": sub_attributes,
}
if attribute_type != Attribute.Type.complex:
kwargs["uniqueness"] = model.get_field_annotation(attribute_name, Uniqueness)
if attribute_type == Attribute.Type.reference:
kwargs["reference_types"] = root_type.get_scim_reference_types() # type: ignore[attr-defined]

return Attribute(**kwargs)
15 changes: 11 additions & 4 deletions scim2_models/resources/resource_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ..annotations import Mutability
from ..annotations import Required
from ..annotations import Returned
from ..annotations import Uniqueness
from ..attributes import ComplexAttribute
from ..path import URN
from ..reference import URI
Expand Down Expand Up @@ -38,7 +39,13 @@ class SchemaExtension(ComplexAttribute):
class ResourceType(Resource[Any]):
__schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:ResourceType")

name: Annotated[str | None, Mutability.read_only, Required.true] = None
name: Annotated[
str | None,
Mutability.read_only,
Required.true,
CaseExact.true,
Uniqueness.server,
] = None
"""The resource type name.

When applicable, service providers MUST specify the name, e.g.,
Expand All @@ -57,9 +64,9 @@ class ResourceType(Resource[Any]):
This is often the same value as the "name" attribute.
"""

endpoint: Annotated[Reference[URI] | None, Mutability.read_only, Required.true] = (
None
)
endpoint: Annotated[
Reference[URI] | None, Mutability.read_only, Required.true, Uniqueness.server
] = None
"""The resource type's HTTP-addressable endpoint relative to the Base URL,
e.g., '/Users'."""

Expand Down
5 changes: 2 additions & 3 deletions scim2_models/resources/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from typing import TYPE_CHECKING
from typing import Annotated
from typing import ClassVar
from typing import Union

from pydantic import Base64Bytes
from pydantic import EmailStr
Expand Down Expand Up @@ -202,8 +201,8 @@ class GroupMembership(ComplexAttribute):
value: Annotated[str | None, Mutability.read_only] = None
"""The identifier of the User's group."""

ref: Annotated[ # type: ignore[type-arg]
Reference[Union["User", "Group"]] | None,
ref: Annotated[
Reference["Group"] | None,
Mutability.read_only,
] = Field(None, serialization_alias="$ref")
"""The reference URI of a target resource, if the attribute is a
Expand Down
14 changes: 7 additions & 7 deletions tests/test_dynamic_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,9 +857,7 @@ def test_make_user_model_from_schema(load_sample):
assert Groups.get_field_annotation("value", Uniqueness) == Uniqueness.none

# group.ref
assert (
Groups.get_field_root_type("ref") == Reference[Union["User", "Group"]] # noqa: F821
)
assert Groups.get_field_root_type("ref") == Reference["Group"] # noqa: F821
assert not Groups.get_field_multiplicity("ref")
assert (
Groups.model_fields["ref"].description
Expand Down Expand Up @@ -1388,7 +1386,7 @@ def test_make_enterprise_user_model_from_schema(load_sample):
== "The id of the SCIM resource representing the User's manager. REQUIRED."
)
assert Manager.get_field_annotation("value", Required) == Required.true
assert Manager.get_field_annotation("value", CaseExact) == CaseExact.false
assert Manager.get_field_annotation("value", CaseExact) == CaseExact.true
assert Manager.get_field_annotation("value", Mutability) == Mutability.read_write
assert Manager.get_field_annotation("value", Returned) == Returned.default
assert Manager.get_field_annotation("value", Uniqueness) == Uniqueness.none
Expand Down Expand Up @@ -1452,10 +1450,10 @@ def test_make_resource_type_model_from_schema(load_sample):
== "The resource type name. When applicable, service providers MUST specify the name, e.g., 'User'."
)
assert ResourceType.get_field_annotation("name", Required) == Required.true
assert ResourceType.get_field_annotation("name", CaseExact) == CaseExact.false
assert ResourceType.get_field_annotation("name", CaseExact) == CaseExact.true
assert ResourceType.get_field_annotation("name", Mutability) == Mutability.read_only
assert ResourceType.get_field_annotation("name", Returned) == Returned.default
assert ResourceType.get_field_annotation("name", Uniqueness) == Uniqueness.none
assert ResourceType.get_field_annotation("name", Uniqueness) == Uniqueness.server

# description
assert ResourceType.get_field_root_type("description") is str
Expand Down Expand Up @@ -1493,7 +1491,9 @@ def test_make_resource_type_model_from_schema(load_sample):
== Mutability.read_only
)
assert ResourceType.get_field_annotation("endpoint", Returned) == Returned.default
assert ResourceType.get_field_annotation("endpoint", Uniqueness) == Uniqueness.none
assert (
ResourceType.get_field_annotation("endpoint", Uniqueness) == Uniqueness.server
)

# schema
assert ResourceType.get_field_root_type("schema_") == Reference[URI]
Expand Down
Loading