Skip to content

Commit a8d7f93

Browse files
authored
Merge pull request #112 from python-scim/py314
Add support for Python 3.14 and remove support for Python 3.9
2 parents f63cfb6 + 145bb66 commit a8d7f93

37 files changed

+349
-681
lines changed

.github/workflows/tests.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
python:
21+
- '3.14'
2122
- '3.13'
2223
- '3.12'
2324
- '3.11'
2425
- '3.10'
25-
- '3.9'
2626
steps:
2727
- uses: actions/checkout@v4
2828
- name: Install uv
@@ -55,6 +55,7 @@ jobs:
5555
fail-fast: false
5656
matrix:
5757
python:
58+
- '3.14'
5859
- '3.13'
5960
- '3.12'
6061
- '3.11'

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ repos:
33
- repo: https://github.com/astral-sh/ruff-pre-commit
44
rev: 'v0.14.0'
55
hooks:
6-
- id: ruff
6+
- id: ruff-check
77
args: [--fix, --exit-non-zero-on-fix]
88
- id: ruff-format
99
- repo: https://github.com/pre-commit/pre-commit-hooks

doc/changelog.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Changelog
22
=========
33

4+
[0.5.1] - Unreleased
5+
--------------------
6+
7+
Added
8+
^^^^^
9+
- Support for Python 3.14.
10+
11+
Removed
12+
^^^^^^^
13+
- Support for Python 3.9.
14+
415
[0.5.0] - 2025-08-18
516
--------------------
617

doc/tutorial.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ Python models have generally the same name than in the SCIM specifications, they
3030
>>> user = User.model_validate(payload)
3131
>>> user.user_name
3232
'bjensen@example.com'
33-
>>> user.meta.created
34-
datetime.datetime(2010, 1, 23, 4, 56, 22, tzinfo=TzInfo(UTC))
33+
>>> user.meta.created # doctest: +ELLIPSIS
34+
datetime.datetime(2010, 1, 23, 4, 56, 22, tzinfo=...)
3535
3636
3737
Model serialization

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers = [
2525
"Operating System :: OS Independent",
2626
]
2727

28-
requires-python = ">= 3.9"
28+
requires-python = ">= 3.10"
2929
dependencies = [
3030
"pydantic[email]>=2.7.0"
3131
]
@@ -128,11 +128,11 @@ warn_required_dynamic_aliases = true
128128
requires = ["tox>=4.19"]
129129
env_list = [
130130
"style",
131-
"py39",
132131
"py310",
133132
"py311",
134133
"py312",
135134
"py313",
135+
"py314",
136136
"minversions",
137137
"doc",
138138
"coverage",

scim2_models/attributes.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from inspect import isclass
22
from typing import Annotated
33
from typing import Any
4-
from typing import Optional
54
from typing import get_origin
65

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

19-
_attribute_urn: Optional[str] = None
18+
_attribute_urn: str | None = None
2019

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

3130

3231
class MultiValuedComplexAttribute(ComplexAttribute):
33-
type: Optional[str] = None
32+
type: str | None = None
3433
"""A label indicating the attribute's function."""
3534

36-
primary: Optional[bool] = None
35+
primary: bool | None = None
3736
"""A Boolean value indicating the 'primary' or preferred attribute value
3837
for this attribute."""
3938

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

43-
value: Optional[Any] = None
42+
value: Any | None = None
4443
"""The value of an entitlement."""
4544

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

scim2_models/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def annotation_type_filter(item: Any) -> bool:
9696
return field_annotation
9797

9898
@classmethod
99-
def get_field_root_type(cls, attribute_name: str) -> Optional[type]:
99+
def get_field_root_type(cls, attribute_name: str) -> type | None:
100100
"""Extract the root type from a model field.
101101
102102
This method unwraps complex type annotations to find the underlying
@@ -244,7 +244,7 @@ def normalize_dict_keys(
244244
return result
245245

246246
def normalize_value(
247-
val: Any, model_class: Optional[type["BaseModel"]] = None
247+
val: Any, model_class: type["BaseModel"] | None = None
248248
) -> Any:
249249
"""Normalize input value based on model class."""
250250
if not isinstance(val, dict):
@@ -504,7 +504,7 @@ def model_serializer_exclude_none(
504504
def model_validate(
505505
cls,
506506
*args: Any,
507-
scim_ctx: Optional[Context] = Context.DEFAULT,
507+
scim_ctx: Context | None = Context.DEFAULT,
508508
original: Optional["BaseModel"] = None,
509509
**kwargs: Any,
510510
) -> Self:

scim2_models/messages/bulk.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from enum import Enum
22
from typing import Annotated
33
from typing import Any
4-
from typing import Optional
54

65
from pydantic import Field
76
from pydantic import PlainSerializer
@@ -19,30 +18,30 @@ class Method(str, Enum):
1918
patch = "PATCH"
2019
delete = "DELETE"
2120

22-
method: Optional[Method] = None
21+
method: Method | None = None
2322
"""The HTTP method of the current operation."""
2423

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

29-
version: Optional[str] = None
28+
version: str | None = None
3029
"""The current resource version."""
3130

32-
path: Optional[str] = None
31+
path: str | None = None
3332
"""The resource's relative path to the SCIM service provider's root."""
3433

35-
data: Optional[Any] = None
34+
data: Any | None = None
3635
"""The resource data as it would appear for a single SCIM POST, PUT, or
3736
PATCH operation."""
3837

39-
location: Optional[str] = None
38+
location: str | None = None
4039
"""The resource endpoint URL."""
4140

42-
response: Optional[Any] = None
41+
response: Any | None = None
4342
"""The HTTP response body for the specified request operation."""
4443

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

4847

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

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

66-
operations: Optional[list[BulkOperation]] = Field(
65+
operations: list[BulkOperation] | None = Field(
6766
None, serialization_alias="Operations"
6867
)
6968
"""Defines operations within a bulk job."""
@@ -81,7 +80,7 @@ class BulkResponse(Message):
8180
"urn:ietf:params:scim:api:messages:2.0:BulkResponse"
8281
]
8382

84-
operations: Optional[list[BulkOperation]] = Field(
83+
operations: list[BulkOperation] | None = Field(
8584
None, serialization_alias="Operations"
8685
)
8786
"""Defines operations within a bulk job."""

scim2_models/messages/error.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from typing import Annotated
2-
from typing import Optional
32

43
from pydantic import PlainSerializer
54

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

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

22-
scim_type: Optional[str] = None
21+
scim_type: str | None = None
2322
"""A SCIM detail error keyword."""
2423

25-
detail: Optional[str] = None
24+
detail: str | None = None
2625
"""A detailed human-readable message."""
2726

2827
@classmethod

scim2_models/messages/list_response.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from typing import Annotated
22
from typing import Any
33
from typing import Generic
4-
from typing import Optional
54

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

25-
total_results: Optional[int] = None
24+
total_results: int | None = None
2625
"""The total number of results returned by the list or query operation."""
2726

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

32-
items_per_page: Optional[int] = None
31+
items_per_page: int | None = None
3332
"""The number of resources returned in a list response page."""
3433

35-
resources: Optional[list[AnyResource]] = Field(
36-
None, serialization_alias="Resources"
37-
)
34+
resources: list[AnyResource] | None = Field(None, serialization_alias="Resources")
3835
"""A multi-valued list of complex objects containing the requested
3936
resources."""
4037

0 commit comments

Comments
 (0)