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
3 changes: 2 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ jobs:
fail-fast: false
matrix:
python:
- '3.14'
- '3.13'
- '3.12'
- '3.11'
- '3.10'
- '3.9'
steps:
- uses: actions/checkout@v4
- name: Install uv
Expand Down Expand Up @@ -55,6 +55,7 @@ jobs:
fail-fast: false
matrix:
python:
- '3.14'
- '3.13'
- '3.12'
- '3.11'
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.14.0'
hooks:
- id: ruff
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
11 changes: 11 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
=========

[0.5.1] - Unreleased
--------------------

Added
^^^^^
- Support for Python 3.14.

Removed
^^^^^^^
- Support for Python 3.9.

[0.5.0] - 2025-08-18
--------------------

Expand Down
4 changes: 2 additions & 2 deletions doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ Python models have generally the same name than in the SCIM specifications, they
>>> user = User.model_validate(payload)
>>> user.user_name
'bjensen@example.com'
>>> user.meta.created
datetime.datetime(2010, 1, 23, 4, 56, 22, tzinfo=TzInfo(UTC))
>>> user.meta.created # doctest: +ELLIPSIS
datetime.datetime(2010, 1, 23, 4, 56, 22, tzinfo=...)


Model serialization
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ classifiers = [
"Operating System :: OS Independent",
]

requires-python = ">= 3.9"
requires-python = ">= 3.10"
dependencies = [
"pydantic[email]>=2.7.0"
]
Expand Down Expand Up @@ -128,11 +128,11 @@ warn_required_dynamic_aliases = true
requires = ["tox>=4.19"]
env_list = [
"style",
"py39",
"py310",
"py311",
"py312",
"py313",
"py314",
"minversions",
"doc",
"coverage",
Expand Down
13 changes: 6 additions & 7 deletions scim2_models/attributes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from inspect import isclass
from typing import Annotated
from typing import Any
from typing import Optional
from typing import get_origin

from pydantic import Field
Expand All @@ -16,7 +15,7 @@
class ComplexAttribute(BaseModel):
"""A complex attribute as defined in :rfc:`RFC7643 §2.3.8 <7643#section-2.3.8>`."""

_attribute_urn: Optional[str] = None
_attribute_urn: str | None = None

def get_attribute_urn(self, field_name: str) -> str:
"""Build the full URN of the attribute.
Expand All @@ -30,20 +29,20 @@ def get_attribute_urn(self, field_name: str) -> str:


class MultiValuedComplexAttribute(ComplexAttribute):
type: Optional[str] = None
type: str | None = None
"""A label indicating the attribute's function."""

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

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

value: Optional[Any] = None
value: Any | None = None
"""The value of an entitlement."""

ref: Optional[Reference[Any]] = Field(None, serialization_alias="$ref")
ref: Reference[Any] | None = Field(None, serialization_alias="$ref")
"""The reference URI of a target resource, if the attribute is a
reference."""

Expand Down
6 changes: 3 additions & 3 deletions scim2_models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def annotation_type_filter(item: Any) -> bool:
return field_annotation

@classmethod
def get_field_root_type(cls, attribute_name: str) -> Optional[type]:
def get_field_root_type(cls, attribute_name: str) -> type | None:
"""Extract the root type from a model field.

This method unwraps complex type annotations to find the underlying
Expand Down Expand Up @@ -244,7 +244,7 @@ def normalize_dict_keys(
return result

def normalize_value(
val: Any, model_class: Optional[type["BaseModel"]] = None
val: Any, model_class: type["BaseModel"] | None = None
) -> Any:
"""Normalize input value based on model class."""
if not isinstance(val, dict):
Expand Down Expand Up @@ -504,7 +504,7 @@ def model_serializer_exclude_none(
def model_validate(
cls,
*args: Any,
scim_ctx: Optional[Context] = Context.DEFAULT,
scim_ctx: Context | None = Context.DEFAULT,
original: Optional["BaseModel"] = None,
**kwargs: Any,
) -> Self:
Expand Down
23 changes: 11 additions & 12 deletions scim2_models/messages/bulk.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from enum import Enum
from typing import Annotated
from typing import Any
from typing import Optional

from pydantic import Field
from pydantic import PlainSerializer
Expand All @@ -19,30 +18,30 @@ class Method(str, Enum):
patch = "PATCH"
delete = "DELETE"

method: Optional[Method] = None
method: Method | None = None
"""The HTTP method of the current operation."""

bulk_id: Optional[str] = None
bulk_id: str | None = None
"""The transient identifier of a newly created resource, unique within a
bulk request and created by the client."""

version: Optional[str] = None
version: str | None = None
"""The current resource version."""

path: Optional[str] = None
path: str | None = None
"""The resource's relative path to the SCIM service provider's root."""

data: Optional[Any] = None
data: Any | None = None
"""The resource data as it would appear for a single SCIM POST, PUT, or
PATCH operation."""

location: Optional[str] = None
location: str | None = None
"""The resource endpoint URL."""

response: Optional[Any] = None
response: Any | None = None
"""The HTTP response body for the specified request operation."""

status: Annotated[Optional[int], PlainSerializer(_int_to_str)] = None
status: Annotated[int | None, PlainSerializer(_int_to_str)] = None
"""The HTTP response status code for the requested operation."""


Expand All @@ -58,12 +57,12 @@ class BulkRequest(Message):
"urn:ietf:params:scim:api:messages:2.0:BulkRequest"
]

fail_on_errors: Optional[int] = None
fail_on_errors: int | None = None
"""An integer specifying the number of errors that the service provider
will accept before the operation is terminated and an error response is
returned."""

operations: Optional[list[BulkOperation]] = Field(
operations: list[BulkOperation] | None = Field(
None, serialization_alias="Operations"
)
"""Defines operations within a bulk job."""
Expand All @@ -81,7 +80,7 @@ class BulkResponse(Message):
"urn:ietf:params:scim:api:messages:2.0:BulkResponse"
]

operations: Optional[list[BulkOperation]] = Field(
operations: list[BulkOperation] | None = Field(
None, serialization_alias="Operations"
)
"""Defines operations within a bulk job."""
7 changes: 3 additions & 4 deletions scim2_models/messages/error.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Annotated
from typing import Optional

from pydantic import PlainSerializer

Expand All @@ -15,14 +14,14 @@ class Error(Message):
"urn:ietf:params:scim:api:messages:2.0:Error"
]

status: Annotated[Optional[int], PlainSerializer(_int_to_str)] = None
status: Annotated[int | None, PlainSerializer(_int_to_str)] = None
"""The HTTP status code (see Section 6 of [RFC7231]) expressed as a JSON
string."""

scim_type: Optional[str] = None
scim_type: str | None = None
"""A SCIM detail error keyword."""

detail: Optional[str] = None
detail: str | None = None
"""A detailed human-readable message."""

@classmethod
Expand Down
11 changes: 4 additions & 7 deletions scim2_models/messages/list_response.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import Annotated
from typing import Any
from typing import Generic
from typing import Optional

from pydantic import Field
from pydantic import ValidationInfo
Expand All @@ -22,19 +21,17 @@ class ListResponse(Message, Generic[AnyResource], metaclass=_GenericMessageMetac
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
]

total_results: Optional[int] = None
total_results: int | None = None
"""The total number of results returned by the list or query operation."""

start_index: Optional[int] = None
start_index: int | None = None
"""The 1-based index of the first result in the current set of list
results."""

items_per_page: Optional[int] = None
items_per_page: int | None = None
"""The number of resources returned in a list response page."""

resources: Optional[list[AnyResource]] = Field(
None, serialization_alias="Resources"
)
resources: list[AnyResource] | None = Field(None, serialization_alias="Resources")
"""A multi-valued list of complex objects containing the requested
resources."""

Expand Down
9 changes: 4 additions & 5 deletions scim2_models/messages/message.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from collections.abc import Callable
from typing import Annotated
from typing import Any
from typing import Callable
from typing import Optional
from typing import Union
from typing import get_args
from typing import get_origin
Expand All @@ -23,14 +22,14 @@ class Message(ScimObject):

def _create_schema_discriminator(
resource_types_schemas: list[str],
) -> Callable[[Any], Optional[str]]:
) -> Callable[[Any], str | None]:
"""Create a schema discriminator function for the given resource schemas.

:param resource_types_schemas: List of valid resource schemas
:return: Discriminator function for Pydantic
"""

def get_schema_from_payload(payload: Any) -> Optional[str]:
def get_schema_from_payload(payload: Any) -> str | None:
"""Extract schema from SCIM payload for discrimination.

:param payload: SCIM payload dict or object
Expand Down Expand Up @@ -89,7 +88,7 @@ def _create_tagged_resource_union(resource_union: Any) -> Any:
for resource_type in resource_types
]
# Dynamic union construction from tuple - MyPy can't validate this at compile time
union = Union[tuple(tagged_resources)] # type: ignore
union = Union[tuple(tagged_resources)] # type: ignore # noqa: UP007
return Annotated[union, discriminator]


Expand Down
10 changes: 4 additions & 6 deletions scim2_models/messages/patch_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
from typing import Annotated
from typing import Any
from typing import Generic
from typing import Optional
from typing import TypeVar
from typing import Union

from pydantic import Field
from pydantic import ValidationInfo
Expand Down Expand Up @@ -48,7 +46,7 @@ class Op(str, Enum):
despite :rfc:`RFC7644 §3.5.2 <7644#section-3.5.2>`, op is case-insensitive.
"""

path: Optional[str] = None
path: str | None = None
"""The "path" attribute value is a String containing an attribute path
describing the target of the operation."""

Expand Down Expand Up @@ -113,7 +111,7 @@ def validate_operation_requirements(self, info: ValidationInfo) -> Self:

return self

value: Optional[Any] = None
value: Any | None = None

@field_validator("op", mode="before")
@classmethod
Expand Down Expand Up @@ -165,7 +163,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self:
return super().__new__(cls)

def __class_getitem__(
cls, typevar_values: Union[type[Resource[Any]], tuple[type[Resource[Any]], ...]]
cls, typevar_values: type[Resource[Any]] | tuple[type[Resource[Any]], ...]
) -> Any:
"""Validate type parameter when creating parameterized type.

Expand Down Expand Up @@ -211,7 +209,7 @@ def __class_getitem__(
"urn:ietf:params:scim:api:messages:2.0:PatchOp"
]

operations: Annotated[Optional[list[PatchOperation]], Required.true] = Field(
operations: Annotated[list[PatchOperation] | None, Required.true] = Field(
None, serialization_alias="Operations", min_length=1
)
"""The body of an HTTP PATCH request MUST contain the attribute
Expand Down
Loading
Loading