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
1 change: 1 addition & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Fixed
^^^^^
- Attributes with ``None`` type are excluded from Schema generation.
- Allow PATCH operations on resources and extensions root path.
- Multiple ComplexAttribute do not inherit from MultiValuedComplexAttribute by default. :issue:`72` :issue:`73`

[0.4.2] - 2025-08-05
--------------------
Expand Down
29 changes: 29 additions & 0 deletions samples/rfc7643-8.7.2-schema-service_provider_configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,24 @@
"returned": "default",
"mutability": "readOnly",
"subAttributes": [
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "The authentication scheme.",
"required": true,
"canonicalValues": [
"oauth",
"oauth2",
"oauthbearertoken",
"httpbasic",
"httpdigest"
],
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "name",
"type": "string",
Expand Down Expand Up @@ -226,6 +244,17 @@
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "primary",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute.",
"required": false,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion scim2_models/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ def is_complex_attribute(type_: type) -> bool:
return (
get_origin(type_) != Reference
and isclass(type_)
and issubclass(type_, (ComplexAttribute, MultiValuedComplexAttribute))
and issubclass(type_, ComplexAttribute)
)
3 changes: 1 addition & 2 deletions scim2_models/resources/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@
from ..annotations import Mutability
from ..annotations import Required
from ..attributes import ComplexAttribute
from ..attributes import MultiValuedComplexAttribute
from ..reference import Reference
from .resource import Resource


class GroupMember(MultiValuedComplexAttribute):
class GroupMember(ComplexAttribute):
value: Annotated[Optional[str], Mutability.immutable] = None
"""Identifier of the member of this Group."""

Expand Down
6 changes: 1 addition & 5 deletions scim2_models/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from ..annotations import Returned
from ..annotations import Uniqueness
from ..attributes import ComplexAttribute
from ..attributes import MultiValuedComplexAttribute
from ..attributes import is_complex_attribute
from ..base import BaseModel
from ..context import Context
Expand Down Expand Up @@ -438,10 +437,7 @@ def _model_attribute_to_scim_attribute(
sub_attributes = (
[
_model_attribute_to_scim_attribute(root_type, sub_attribute_name)
for sub_attribute_name in _dedicated_attributes(
root_type,
[MultiValuedComplexAttribute],
)
for sub_attribute_name in root_type.model_fields # type: ignore
if (
attribute_name != "sub_attributes"
or sub_attribute_name != "sub_attributes"
Expand Down
15 changes: 4 additions & 11 deletions scim2_models/resources/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from ..annotations import Returned
from ..annotations import Uniqueness
from ..attributes import ComplexAttribute
from ..attributes import MultiValuedComplexAttribute
from ..attributes import is_complex_attribute
from ..base import BaseModel
from ..constants import RESERVED_WORDS
Expand All @@ -49,7 +48,6 @@ def _make_python_identifier(identifier: str) -> str:
def _make_python_model(
obj: Union["Schema", "Attribute"],
base: type[T],
multiple: bool = False,
) -> type[T]:
"""Build a Python model from a Schema or an Attribute object."""
if isinstance(obj, Attribute):
Expand Down Expand Up @@ -99,7 +97,6 @@ class Type(str, Enum):

def _to_python(
self,
multiple: bool = False,
reference_types: Optional[list[str]] = None,
) -> type:
if self.value == self.reference and reference_types is not None:
Expand All @@ -119,9 +116,7 @@ def _to_python(
self.integer: int,
self.date_time: datetime,
self.binary: Base64Bytes,
self.complex: MultiValuedComplexAttribute
if multiple
else ComplexAttribute,
self.complex: ComplexAttribute,
}
return attr_types[self.value]

Expand Down Expand Up @@ -215,12 +210,10 @@ def _to_python(self) -> Optional[tuple[Any, Any]]:
if not self.name or not self.type:
return None

attr_type = self.type._to_python(bool(self.multi_valued), self.reference_types)
attr_type = self.type._to_python(self.reference_types)

if attr_type in (ComplexAttribute, MultiValuedComplexAttribute):
attr_type = _make_python_model(
obj=self, base=attr_type, multiple=bool(self.multi_valued)
)
if attr_type == ComplexAttribute:
attr_type = _make_python_model(obj=self, base=attr_type)

if self.multi_valued:
attr_type = list[attr_type] # type: ignore
Expand Down
4 changes: 2 additions & 2 deletions scim2_models/resources/service_provider_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Filter(ComplexAttribute):
"""A Boolean value specifying whether or not the operation is supported."""

max_results: Annotated[Optional[int], Mutability.read_only, Required.true] = None
"""A Boolean value specifying whether or not the operation is supported."""
"""An integer value specifying the maximum number of resources returned in a response."""


class ChangePassword(ComplexAttribute):
Expand Down Expand Up @@ -66,7 +66,7 @@ class Type(str, Enum):

type: Annotated[Optional[Type], Mutability.read_only, Required.true] = Field(
None,
examples=["oauth", "oauth2", "oauthbreakertoken", "httpbasic", "httpdigest"],
examples=["oauth", "oauth2", "oauthbearertoken", "httpbasic", "httpdigest"],
)
"""The authentication scheme."""

Expand Down
55 changes: 43 additions & 12 deletions scim2_models/resources/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from ..annotations import Returned
from ..annotations import Uniqueness
from ..attributes import ComplexAttribute
from ..attributes import MultiValuedComplexAttribute
from ..reference import ExternalReference
from ..reference import Reference
from ..utils import Base64Bytes
Expand Down Expand Up @@ -48,7 +47,7 @@ class Name(ComplexAttribute):
languages (e.g., 'III' given the full name 'Ms. Barbara J Jensen, III')."""


class Email(MultiValuedComplexAttribute):
class Email(ComplexAttribute):
class Type(str, Enum):
work = "work"
home = "home"
Expand All @@ -69,7 +68,7 @@ class Type(str, Enum):
address."""


class PhoneNumber(MultiValuedComplexAttribute):
class PhoneNumber(ComplexAttribute):
class Type(str, Enum):
work = "work"
home = "home"
Expand All @@ -96,7 +95,7 @@ class Type(str, Enum):
number."""


class Im(MultiValuedComplexAttribute):
class Im(ComplexAttribute):
class Type(str, Enum):
aim = "aim"
gtalk = "gtalk"
Expand Down Expand Up @@ -124,7 +123,7 @@ class Type(str, Enum):
for this attribute, e.g., the preferred messenger or primary messenger."""


class Photo(MultiValuedComplexAttribute):
class Photo(ComplexAttribute):
class Type(str, Enum):
photo = "photo"
thumbnail = "thumbnail"
Expand All @@ -144,7 +143,7 @@ class Type(str, Enum):
for this attribute, e.g., the preferred photo or thumbnail."""


class Address(MultiValuedComplexAttribute):
class Address(ComplexAttribute):
class Type(str, Enum):
work = "work"
home = "home"
Expand Down Expand Up @@ -181,11 +180,22 @@ class Type(str, Enum):
for this attribute, e.g., the preferred photo or thumbnail."""


class Entitlement(MultiValuedComplexAttribute):
pass
class Entitlement(ComplexAttribute):
value: Optional[str] = None
"""The value of an entitlement."""

display: Optional[str] = None
"""A human-readable name, primarily used for display purposes."""

type: Optional[str] = None
"""A label indicating the attribute's function."""

primary: Optional[bool] = None
"""A Boolean value indicating the 'primary' or preferred attribute value
for this attribute."""


class GroupMembership(MultiValuedComplexAttribute):
class GroupMembership(ComplexAttribute):
value: Annotated[Optional[str], Mutability.read_only] = None
"""The identifier of the User's group."""

Expand All @@ -206,14 +216,35 @@ class GroupMembership(MultiValuedComplexAttribute):
'indirect'."""


class Role(MultiValuedComplexAttribute):
pass
class Role(ComplexAttribute):
value: Optional[str] = None
"""The value of a role."""

display: Optional[str] = None
"""A human-readable name, primarily used for display purposes."""

type: Optional[str] = None
"""A label indicating the attribute's function."""

class X509Certificate(MultiValuedComplexAttribute):
primary: Optional[bool] = None
"""A Boolean value indicating the 'primary' or preferred attribute value
for this attribute."""


class X509Certificate(ComplexAttribute):
value: Annotated[Optional[Base64Bytes], CaseExact.true] = None
"""The value of an X.509 certificate."""

display: Optional[str] = None
"""A human-readable name, primarily used for display purposes."""

type: Optional[str] = None
"""A label indicating the attribute's function."""

primary: Optional[bool] = None
"""A Boolean value indicating the 'primary' or preferred attribute value
for this attribute."""


class User(Resource[AnyExtension]):
schemas: Annotated[list[str], Required.true] = [
Expand Down
23 changes: 11 additions & 12 deletions tests/test_dynamic_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from scim2_models.annotations import Returned
from scim2_models.annotations import Uniqueness
from scim2_models.attributes import ComplexAttribute
from scim2_models.attributes import MultiValuedComplexAttribute
from scim2_models.reference import ExternalReference
from scim2_models.reference import Reference
from scim2_models.reference import URIReference
Expand Down Expand Up @@ -408,7 +407,7 @@ def test_make_user_model_from_schema(load_sample):
# emails
Emails = User.get_field_root_type("emails")
assert Emails == User.Emails
assert issubclass(Emails, MultiValuedComplexAttribute)
assert issubclass(Emails, ComplexAttribute)
assert User.get_field_multiplicity("emails")
assert (
User.model_fields["emails"].description
Expand Down Expand Up @@ -476,7 +475,7 @@ def test_make_user_model_from_schema(load_sample):
# phone_numbers
PhoneNumbers = User.get_field_root_type("phone_numbers")
assert PhoneNumbers == User.PhoneNumbers
assert issubclass(PhoneNumbers, MultiValuedComplexAttribute)
assert issubclass(PhoneNumbers, ComplexAttribute)
assert User.get_field_multiplicity("phone_numbers")
assert (
User.model_fields["phone_numbers"].description
Expand Down Expand Up @@ -560,7 +559,7 @@ def test_make_user_model_from_schema(load_sample):
# ims
Ims = User.get_field_root_type("ims")
assert Ims == User.Ims
assert issubclass(Ims, MultiValuedComplexAttribute)
assert issubclass(Ims, ComplexAttribute)
assert User.get_field_multiplicity("ims")
assert (
User.model_fields["ims"].description
Expand Down Expand Up @@ -637,7 +636,7 @@ def test_make_user_model_from_schema(load_sample):
# photos
Photos = User.get_field_root_type("photos")
assert Photos == User.Photos
assert issubclass(Photos, MultiValuedComplexAttribute)
assert issubclass(Photos, ComplexAttribute)
assert User.get_field_multiplicity("photos")
assert User.model_fields["photos"].description == "URLs of photos of the User."
assert User.get_field_annotation("photos", Required) == Required.false
Expand Down Expand Up @@ -699,7 +698,7 @@ def test_make_user_model_from_schema(load_sample):
# addresses
Addresses = User.get_field_root_type("addresses")
assert Addresses == User.Addresses
assert issubclass(Addresses, MultiValuedComplexAttribute)
assert issubclass(Addresses, ComplexAttribute)
assert User.get_field_multiplicity("addresses")
assert (
User.model_fields["addresses"].description
Expand Down Expand Up @@ -837,7 +836,7 @@ def test_make_user_model_from_schema(load_sample):
# groups
Groups = User.get_field_root_type("groups")
assert Groups == User.Groups
assert issubclass(Groups, MultiValuedComplexAttribute)
assert issubclass(Groups, ComplexAttribute)
assert User.get_field_multiplicity("groups")
assert (
User.model_fields["groups"].description
Expand Down Expand Up @@ -911,7 +910,7 @@ def test_make_user_model_from_schema(load_sample):
# entitlements
Entitlements = User.get_field_root_type("entitlements")
assert Entitlements == User.Entitlements
assert issubclass(Entitlements, MultiValuedComplexAttribute)
assert issubclass(Entitlements, ComplexAttribute)
assert User.get_field_multiplicity("entitlements")
assert (
User.model_fields["entitlements"].description
Expand Down Expand Up @@ -989,7 +988,7 @@ def test_make_user_model_from_schema(load_sample):
# roles
Roles = User.get_field_root_type("roles")
assert Roles == User.Roles
assert issubclass(Roles, MultiValuedComplexAttribute)
assert issubclass(Roles, ComplexAttribute)
assert User.get_field_multiplicity("roles")
assert (
User.model_fields["roles"].description
Expand Down Expand Up @@ -1053,7 +1052,7 @@ def test_make_user_model_from_schema(load_sample):
# x_509_certificates
X509Certificates = User.get_field_root_type("x_509_certificates")
assert X509Certificates == User.X509Certificates
assert issubclass(X509Certificates, MultiValuedComplexAttribute)
assert issubclass(X509Certificates, ComplexAttribute)
assert User.get_field_multiplicity("x_509_certificates")
assert (
User.model_fields["x_509_certificates"].description
Expand Down Expand Up @@ -2210,7 +2209,7 @@ def test_make_schema_model_from_schema(load_sample):
# attributes
Attributes = Schema_.get_field_root_type("attributes")
assert Attributes == Schema_.Attributes
assert issubclass(Attributes, MultiValuedComplexAttribute)
assert issubclass(Attributes, ComplexAttribute)
assert Schema_.get_field_multiplicity("attributes")
assert (
Schema_.model_fields["attributes"].description
Expand Down Expand Up @@ -2440,7 +2439,7 @@ def test_make_schema_model_from_schema(load_sample):
# sub_attributes
SubAttributes = Attributes.get_field_root_type("sub_attributes")
assert SubAttributes == Attributes.SubAttributes
assert issubclass(SubAttributes, MultiValuedComplexAttribute)
assert issubclass(SubAttributes, ComplexAttribute)
assert Attributes.get_field_multiplicity("sub_attributes")
assert (
Attributes.model_fields["sub_attributes"].description
Expand Down
Loading
Loading