scim2-models 0.4.2__py3-none-any.whl → 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scim2_models/attributes.py +7 -8
- scim2_models/base.py +3 -3
- scim2_models/messages/bulk.py +11 -12
- scim2_models/messages/error.py +3 -4
- scim2_models/messages/list_response.py +4 -7
- scim2_models/messages/message.py +4 -5
- scim2_models/messages/patch_op.py +23 -10
- scim2_models/messages/search_request.py +15 -16
- scim2_models/reference.py +1 -1
- scim2_models/resources/enterprise_user.py +9 -10
- scim2_models/resources/group.py +7 -10
- scim2_models/resources/resource.py +26 -31
- scim2_models/resources/resource_type.py +8 -9
- scim2_models/resources/schema.py +26 -31
- scim2_models/resources/service_provider_config.py +28 -31
- scim2_models/resources/user.py +99 -70
- scim2_models/scim_object.py +3 -4
- scim2_models/urn.py +27 -12
- scim2_models/utils.py +14 -7
- scim2_models-0.5.1.dist-info/METADATA +280 -0
- scim2_models-0.5.1.dist-info/RECORD +29 -0
- scim2_models-0.5.1.dist-info/WHEEL +4 -0
- scim2_models-0.4.2.dist-info/METADATA +0 -280
- scim2_models-0.4.2.dist-info/RECORD +0 -30
- scim2_models-0.4.2.dist-info/WHEEL +0 -4
- scim2_models-0.4.2.dist-info/licenses/LICENSE +0 -201
scim2_models/resources/user.py
CHANGED
|
@@ -2,8 +2,6 @@ from enum import Enum
|
|
|
2
2
|
from typing import Annotated
|
|
3
3
|
from typing import ClassVar
|
|
4
4
|
from typing import Literal
|
|
5
|
-
from typing import Optional
|
|
6
|
-
from typing import Union
|
|
7
5
|
|
|
8
6
|
from pydantic import EmailStr
|
|
9
7
|
from pydantic import Field
|
|
@@ -14,7 +12,6 @@ from ..annotations import Required
|
|
|
14
12
|
from ..annotations import Returned
|
|
15
13
|
from ..annotations import Uniqueness
|
|
16
14
|
from ..attributes import ComplexAttribute
|
|
17
|
-
from ..attributes import MultiValuedComplexAttribute
|
|
18
15
|
from ..reference import ExternalReference
|
|
19
16
|
from ..reference import Reference
|
|
20
17
|
from ..utils import Base64Bytes
|
|
@@ -23,53 +20,53 @@ from .resource import Resource
|
|
|
23
20
|
|
|
24
21
|
|
|
25
22
|
class Name(ComplexAttribute):
|
|
26
|
-
formatted:
|
|
23
|
+
formatted: str | None = None
|
|
27
24
|
"""The full name, including all middle names, titles, and suffixes as
|
|
28
25
|
appropriate, formatted for display (e.g., 'Ms. Barbara J Jensen, III')."""
|
|
29
26
|
|
|
30
|
-
family_name:
|
|
27
|
+
family_name: str | None = None
|
|
31
28
|
"""The family name of the User, or last name in most Western languages
|
|
32
29
|
(e.g., 'Jensen' given the full name 'Ms. Barbara J Jensen, III')."""
|
|
33
30
|
|
|
34
|
-
given_name:
|
|
31
|
+
given_name: str | None = None
|
|
35
32
|
"""The given name of the User, or first name in most Western languages
|
|
36
33
|
(e.g., 'Barbara' given the full name 'Ms. Barbara J Jensen, III')."""
|
|
37
34
|
|
|
38
|
-
middle_name:
|
|
35
|
+
middle_name: str | None = None
|
|
39
36
|
"""The middle name(s) of the User (e.g., 'Jane' given the full name 'Ms.
|
|
40
37
|
Barbara J Jensen, III')."""
|
|
41
38
|
|
|
42
|
-
honorific_prefix:
|
|
39
|
+
honorific_prefix: str | None = None
|
|
43
40
|
"""The honorific prefix(es) of the User, or title in most Western languages
|
|
44
41
|
(e.g., 'Ms.' given the full name 'Ms. Barbara J Jensen, III')."""
|
|
45
42
|
|
|
46
|
-
honorific_suffix:
|
|
43
|
+
honorific_suffix: str | None = None
|
|
47
44
|
"""The honorific suffix(es) of the User, or suffix in most Western
|
|
48
45
|
languages (e.g., 'III' given the full name 'Ms. Barbara J Jensen, III')."""
|
|
49
46
|
|
|
50
47
|
|
|
51
|
-
class Email(
|
|
48
|
+
class Email(ComplexAttribute):
|
|
52
49
|
class Type(str, Enum):
|
|
53
50
|
work = "work"
|
|
54
51
|
home = "home"
|
|
55
52
|
other = "other"
|
|
56
53
|
|
|
57
|
-
value:
|
|
54
|
+
value: EmailStr | None = None
|
|
58
55
|
"""Email addresses for the user."""
|
|
59
56
|
|
|
60
|
-
display:
|
|
57
|
+
display: str | None = None
|
|
61
58
|
"""A human-readable name, primarily used for display purposes."""
|
|
62
59
|
|
|
63
|
-
type:
|
|
60
|
+
type: Type | None = Field(None, examples=["work", "home", "other"])
|
|
64
61
|
"""A label indicating the attribute's function, e.g., 'work' or 'home'."""
|
|
65
62
|
|
|
66
|
-
primary:
|
|
63
|
+
primary: bool | None = None
|
|
67
64
|
"""A Boolean value indicating the 'primary' or preferred attribute value
|
|
68
65
|
for this attribute, e.g., the preferred mailing address or primary email
|
|
69
66
|
address."""
|
|
70
67
|
|
|
71
68
|
|
|
72
|
-
class PhoneNumber(
|
|
69
|
+
class PhoneNumber(ComplexAttribute):
|
|
73
70
|
class Type(str, Enum):
|
|
74
71
|
work = "work"
|
|
75
72
|
home = "home"
|
|
@@ -78,25 +75,25 @@ class PhoneNumber(MultiValuedComplexAttribute):
|
|
|
78
75
|
pager = "pager"
|
|
79
76
|
other = "other"
|
|
80
77
|
|
|
81
|
-
value:
|
|
78
|
+
value: str | None = None
|
|
82
79
|
"""Phone number of the User."""
|
|
83
80
|
|
|
84
|
-
display:
|
|
81
|
+
display: str | None = None
|
|
85
82
|
"""A human-readable name, primarily used for display purposes."""
|
|
86
83
|
|
|
87
|
-
type:
|
|
84
|
+
type: Type | None = Field(
|
|
88
85
|
None, examples=["work", "home", "mobile", "fax", "pager", "other"]
|
|
89
86
|
)
|
|
90
87
|
"""A label indicating the attribute's function, e.g., 'work', 'home',
|
|
91
88
|
'mobile'."""
|
|
92
89
|
|
|
93
|
-
primary:
|
|
90
|
+
primary: bool | None = None
|
|
94
91
|
"""A Boolean value indicating the 'primary' or preferred attribute value
|
|
95
92
|
for this attribute, e.g., the preferred phone number or primary phone
|
|
96
93
|
number."""
|
|
97
94
|
|
|
98
95
|
|
|
99
|
-
class Im(
|
|
96
|
+
class Im(ComplexAttribute):
|
|
100
97
|
class Type(str, Enum):
|
|
101
98
|
aim = "aim"
|
|
102
99
|
gtalk = "gtalk"
|
|
@@ -107,214 +104,246 @@ class Im(MultiValuedComplexAttribute):
|
|
|
107
104
|
qq = "qq"
|
|
108
105
|
yahoo = "yahoo"
|
|
109
106
|
|
|
110
|
-
value:
|
|
107
|
+
value: str | None = None
|
|
111
108
|
"""Instant messaging address for the User."""
|
|
112
109
|
|
|
113
|
-
display:
|
|
110
|
+
display: str | None = None
|
|
114
111
|
"""A human-readable name, primarily used for display purposes."""
|
|
115
112
|
|
|
116
|
-
type:
|
|
113
|
+
type: Type | None = Field(
|
|
117
114
|
None, examples=["aim", "gtalk", "icq", "xmpp", "msn", "skype", "qq", "yahoo"]
|
|
118
115
|
)
|
|
119
116
|
"""A label indicating the attribute's function, e.g., 'aim', 'gtalk',
|
|
120
117
|
'xmpp'."""
|
|
121
118
|
|
|
122
|
-
primary:
|
|
119
|
+
primary: bool | None = None
|
|
123
120
|
"""A Boolean value indicating the 'primary' or preferred attribute value
|
|
124
121
|
for this attribute, e.g., the preferred messenger or primary messenger."""
|
|
125
122
|
|
|
126
123
|
|
|
127
|
-
class Photo(
|
|
124
|
+
class Photo(ComplexAttribute):
|
|
128
125
|
class Type(str, Enum):
|
|
129
126
|
photo = "photo"
|
|
130
127
|
thumbnail = "thumbnail"
|
|
131
128
|
|
|
132
|
-
value: Annotated[
|
|
129
|
+
value: Annotated[Reference[ExternalReference] | None, CaseExact.true] = None
|
|
133
130
|
"""URL of a photo of the User."""
|
|
134
131
|
|
|
135
|
-
display:
|
|
132
|
+
display: str | None = None
|
|
136
133
|
"""A human-readable name, primarily used for display purposes."""
|
|
137
134
|
|
|
138
|
-
type:
|
|
135
|
+
type: Type | None = Field(None, examples=["photo", "thumbnail"])
|
|
139
136
|
"""A label indicating the attribute's function, i.e., 'photo' or
|
|
140
137
|
'thumbnail'."""
|
|
141
138
|
|
|
142
|
-
primary:
|
|
139
|
+
primary: bool | None = None
|
|
143
140
|
"""A Boolean value indicating the 'primary' or preferred attribute value
|
|
144
141
|
for this attribute, e.g., the preferred photo or thumbnail."""
|
|
145
142
|
|
|
146
143
|
|
|
147
|
-
class Address(
|
|
144
|
+
class Address(ComplexAttribute):
|
|
148
145
|
class Type(str, Enum):
|
|
149
146
|
work = "work"
|
|
150
147
|
home = "home"
|
|
151
148
|
other = "other"
|
|
152
149
|
|
|
153
|
-
formatted:
|
|
150
|
+
formatted: str | None = None
|
|
154
151
|
"""The full mailing address, formatted for display or use with a mailing
|
|
155
152
|
label."""
|
|
156
153
|
|
|
157
|
-
street_address:
|
|
154
|
+
street_address: str | None = None
|
|
158
155
|
"""The full street address component, which may include house number,
|
|
159
156
|
street name, P.O.
|
|
160
157
|
|
|
161
158
|
box, and multi-line extended street address information.
|
|
162
159
|
"""
|
|
163
160
|
|
|
164
|
-
locality:
|
|
161
|
+
locality: str | None = None
|
|
165
162
|
"""The city or locality component."""
|
|
166
163
|
|
|
167
|
-
region:
|
|
164
|
+
region: str | None = None
|
|
168
165
|
"""The state or region component."""
|
|
169
166
|
|
|
170
|
-
postal_code:
|
|
167
|
+
postal_code: str | None = None
|
|
171
168
|
"""The zip code or postal code component."""
|
|
172
169
|
|
|
173
|
-
country:
|
|
170
|
+
country: str | None = None
|
|
174
171
|
"""The country name component."""
|
|
175
172
|
|
|
176
|
-
type:
|
|
173
|
+
type: Type | None = Field(None, examples=["work", "home", "other"])
|
|
177
174
|
"""A label indicating the attribute's function, e.g., 'work' or 'home'."""
|
|
178
175
|
|
|
179
|
-
primary:
|
|
176
|
+
primary: bool | None = None
|
|
180
177
|
"""A Boolean value indicating the 'primary' or preferred attribute value
|
|
181
178
|
for this attribute, e.g., the preferred photo or thumbnail."""
|
|
182
179
|
|
|
183
180
|
|
|
184
|
-
class Entitlement(
|
|
185
|
-
|
|
181
|
+
class Entitlement(ComplexAttribute):
|
|
182
|
+
value: str | None = None
|
|
183
|
+
"""The value of an entitlement."""
|
|
186
184
|
|
|
185
|
+
display: str | None = None
|
|
186
|
+
"""A human-readable name, primarily used for display purposes."""
|
|
187
|
+
|
|
188
|
+
type: str | None = None
|
|
189
|
+
"""A label indicating the attribute's function."""
|
|
190
|
+
|
|
191
|
+
primary: bool | None = None
|
|
192
|
+
"""A Boolean value indicating the 'primary' or preferred attribute value
|
|
193
|
+
for this attribute."""
|
|
187
194
|
|
|
188
|
-
|
|
189
|
-
|
|
195
|
+
|
|
196
|
+
class GroupMembership(ComplexAttribute):
|
|
197
|
+
value: Annotated[str | None, Mutability.read_only] = None
|
|
190
198
|
"""The identifier of the User's group."""
|
|
191
199
|
|
|
192
200
|
ref: Annotated[
|
|
193
|
-
|
|
201
|
+
Reference[Literal["User"] | Literal["Group"]] | None,
|
|
194
202
|
Mutability.read_only,
|
|
195
203
|
] = Field(None, serialization_alias="$ref")
|
|
196
204
|
"""The reference URI of a target resource, if the attribute is a
|
|
197
205
|
reference."""
|
|
198
206
|
|
|
199
|
-
display: Annotated[
|
|
207
|
+
display: Annotated[str | None, Mutability.read_only] = None
|
|
200
208
|
"""A human-readable name, primarily used for display purposes."""
|
|
201
209
|
|
|
202
|
-
type: Annotated[
|
|
210
|
+
type: Annotated[str | None, Mutability.read_only] = Field(
|
|
203
211
|
None, examples=["direct", "indirect"]
|
|
204
212
|
)
|
|
205
213
|
"""A label indicating the attribute's function, e.g., 'direct' or
|
|
206
214
|
'indirect'."""
|
|
207
215
|
|
|
208
216
|
|
|
209
|
-
class Role(
|
|
210
|
-
|
|
217
|
+
class Role(ComplexAttribute):
|
|
218
|
+
value: str | None = None
|
|
219
|
+
"""The value of a role."""
|
|
220
|
+
|
|
221
|
+
display: str | None = None
|
|
222
|
+
"""A human-readable name, primarily used for display purposes."""
|
|
211
223
|
|
|
224
|
+
type: str | None = None
|
|
225
|
+
"""A label indicating the attribute's function."""
|
|
212
226
|
|
|
213
|
-
|
|
214
|
-
value
|
|
227
|
+
primary: bool | None = None
|
|
228
|
+
"""A Boolean value indicating the 'primary' or preferred attribute value
|
|
229
|
+
for this attribute."""
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class X509Certificate(ComplexAttribute):
|
|
233
|
+
value: Annotated[Base64Bytes | None, CaseExact.true] = None
|
|
215
234
|
"""The value of an X.509 certificate."""
|
|
216
235
|
|
|
236
|
+
display: str | None = None
|
|
237
|
+
"""A human-readable name, primarily used for display purposes."""
|
|
238
|
+
|
|
239
|
+
type: str | None = None
|
|
240
|
+
"""A label indicating the attribute's function."""
|
|
241
|
+
|
|
242
|
+
primary: bool | None = None
|
|
243
|
+
"""A Boolean value indicating the 'primary' or preferred attribute value
|
|
244
|
+
for this attribute."""
|
|
245
|
+
|
|
217
246
|
|
|
218
247
|
class User(Resource[AnyExtension]):
|
|
219
248
|
schemas: Annotated[list[str], Required.true] = [
|
|
220
249
|
"urn:ietf:params:scim:schemas:core:2.0:User"
|
|
221
250
|
]
|
|
222
251
|
|
|
223
|
-
user_name: Annotated[
|
|
252
|
+
user_name: Annotated[str | None, Uniqueness.server, Required.true] = None
|
|
224
253
|
"""Unique identifier for the User, typically used by the user to directly
|
|
225
254
|
authenticate to the service provider."""
|
|
226
255
|
|
|
227
|
-
name:
|
|
256
|
+
name: Name | None = None
|
|
228
257
|
"""The components of the user's real name."""
|
|
229
258
|
|
|
230
259
|
Name: ClassVar[type[ComplexAttribute]] = Name
|
|
231
260
|
|
|
232
|
-
display_name:
|
|
261
|
+
display_name: str | None = None
|
|
233
262
|
"""The name of the User, suitable for display to end-users."""
|
|
234
263
|
|
|
235
|
-
nick_name:
|
|
264
|
+
nick_name: str | None = None
|
|
236
265
|
"""The casual way to address the user in real life, e.g., 'Bob' or 'Bobby'
|
|
237
266
|
instead of 'Robert'."""
|
|
238
267
|
|
|
239
|
-
profile_url:
|
|
268
|
+
profile_url: Reference[ExternalReference] | None = None
|
|
240
269
|
"""A fully qualified URL pointing to a page representing the User's online
|
|
241
270
|
profile."""
|
|
242
271
|
|
|
243
|
-
title:
|
|
272
|
+
title: str | None = None
|
|
244
273
|
"""The user's title, such as "Vice President"."""
|
|
245
274
|
|
|
246
|
-
user_type:
|
|
275
|
+
user_type: str | None = None
|
|
247
276
|
"""Used to identify the relationship between the organization and the user.
|
|
248
277
|
|
|
249
278
|
Typical values used might be 'Contractor', 'Employee', 'Intern',
|
|
250
279
|
'Temp', 'External', and 'Unknown', but any value may be used.
|
|
251
280
|
"""
|
|
252
281
|
|
|
253
|
-
preferred_language:
|
|
282
|
+
preferred_language: str | None = None
|
|
254
283
|
"""Indicates the User's preferred written or spoken language.
|
|
255
284
|
|
|
256
285
|
Generally used for selecting a localized user interface; e.g.,
|
|
257
286
|
'en_US' specifies the language English and country US.
|
|
258
287
|
"""
|
|
259
288
|
|
|
260
|
-
locale:
|
|
289
|
+
locale: str | None = None
|
|
261
290
|
"""Used to indicate the User's default location for purposes of localizing
|
|
262
291
|
items such as currency, date time format, or numerical representations."""
|
|
263
292
|
|
|
264
|
-
timezone:
|
|
293
|
+
timezone: str | None = None
|
|
265
294
|
"""The User's time zone in the 'Olson' time zone database format, e.g.,
|
|
266
295
|
'America/Los_Angeles'."""
|
|
267
296
|
|
|
268
|
-
active:
|
|
297
|
+
active: bool | None = None
|
|
269
298
|
"""A Boolean value indicating the User's administrative status."""
|
|
270
299
|
|
|
271
|
-
password: Annotated[
|
|
300
|
+
password: Annotated[str | None, Mutability.write_only, Returned.never] = None
|
|
272
301
|
"""The User's cleartext password."""
|
|
273
302
|
|
|
274
|
-
emails:
|
|
303
|
+
emails: list[Email] | None = None
|
|
275
304
|
"""Email addresses for the user."""
|
|
276
305
|
|
|
277
306
|
Emails: ClassVar[type[ComplexAttribute]] = Email
|
|
278
307
|
|
|
279
|
-
phone_numbers:
|
|
308
|
+
phone_numbers: list[PhoneNumber] | None = None
|
|
280
309
|
"""Phone numbers for the User."""
|
|
281
310
|
|
|
282
311
|
PhoneNumbers: ClassVar[type[ComplexAttribute]] = PhoneNumber
|
|
283
312
|
|
|
284
|
-
ims:
|
|
313
|
+
ims: list[Im] | None = None
|
|
285
314
|
"""Instant messaging addresses for the User."""
|
|
286
315
|
|
|
287
316
|
Ims: ClassVar[type[ComplexAttribute]] = Im
|
|
288
317
|
|
|
289
|
-
photos:
|
|
318
|
+
photos: list[Photo] | None = None
|
|
290
319
|
"""URLs of photos of the User."""
|
|
291
320
|
|
|
292
321
|
Photos: ClassVar[type[ComplexAttribute]] = Photo
|
|
293
322
|
|
|
294
|
-
addresses:
|
|
323
|
+
addresses: list[Address] | None = None
|
|
295
324
|
"""A physical mailing address for this User."""
|
|
296
325
|
|
|
297
326
|
Addresses: ClassVar[type[ComplexAttribute]] = Address
|
|
298
327
|
|
|
299
|
-
groups: Annotated[
|
|
328
|
+
groups: Annotated[list[GroupMembership] | None, Mutability.read_only] = None
|
|
300
329
|
"""A list of groups to which the user belongs, either through direct
|
|
301
330
|
membership, through nested groups, or dynamically calculated."""
|
|
302
331
|
|
|
303
332
|
Groups: ClassVar[type[ComplexAttribute]] = GroupMembership
|
|
304
333
|
|
|
305
|
-
entitlements:
|
|
334
|
+
entitlements: list[Entitlement] | None = None
|
|
306
335
|
"""A list of entitlements for the User that represent a thing the User
|
|
307
336
|
has."""
|
|
308
337
|
|
|
309
338
|
Entitlements: ClassVar[type[ComplexAttribute]] = Entitlement
|
|
310
339
|
|
|
311
|
-
roles:
|
|
340
|
+
roles: list[Role] | None = None
|
|
312
341
|
"""A list of roles for the User that collectively represent who the User
|
|
313
342
|
is, e.g., 'Student', 'Faculty'."""
|
|
314
343
|
|
|
315
344
|
Roles: ClassVar[type[ComplexAttribute]] = Role
|
|
316
345
|
|
|
317
|
-
x509_certificates:
|
|
346
|
+
x509_certificates: list[X509Certificate] | None = None
|
|
318
347
|
"""A list of certificates issued to the User."""
|
|
319
348
|
|
|
320
349
|
X509Certificates: ClassVar[type[ComplexAttribute]] = X509Certificate
|
scim2_models/scim_object.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Any
|
|
6
|
-
from typing import Optional
|
|
7
6
|
|
|
8
7
|
from .annotations import Required
|
|
9
8
|
from .base import BaseModel
|
|
@@ -22,7 +21,7 @@ class ScimObject(BaseModel):
|
|
|
22
21
|
|
|
23
22
|
def _prepare_model_dump(
|
|
24
23
|
self,
|
|
25
|
-
scim_ctx:
|
|
24
|
+
scim_ctx: Context | None = Context.DEFAULT,
|
|
26
25
|
**kwargs: Any,
|
|
27
26
|
) -> dict[str, Any]:
|
|
28
27
|
kwargs.setdefault("context", {}).setdefault("scim", scim_ctx)
|
|
@@ -36,7 +35,7 @@ class ScimObject(BaseModel):
|
|
|
36
35
|
def model_dump(
|
|
37
36
|
self,
|
|
38
37
|
*args: Any,
|
|
39
|
-
scim_ctx:
|
|
38
|
+
scim_ctx: Context | None = Context.DEFAULT,
|
|
40
39
|
**kwargs: Any,
|
|
41
40
|
) -> dict[str, Any]:
|
|
42
41
|
"""Create a model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump`.
|
|
@@ -53,7 +52,7 @@ class ScimObject(BaseModel):
|
|
|
53
52
|
def model_dump_json(
|
|
54
53
|
self,
|
|
55
54
|
*args: Any,
|
|
56
|
-
scim_ctx:
|
|
55
|
+
scim_ctx: Context | None = Context.DEFAULT,
|
|
57
56
|
**kwargs: Any,
|
|
58
57
|
) -> str:
|
|
59
58
|
"""Create a JSON model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump_json`.
|
scim2_models/urn.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
2
|
from typing import Any
|
|
3
|
-
from typing import Optional
|
|
4
3
|
from typing import Union
|
|
5
4
|
|
|
6
5
|
from .base import BaseModel
|
|
6
|
+
from .utils import _get_path_parts
|
|
7
7
|
from .utils import _normalize_attribute_name
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
@@ -23,12 +23,22 @@ def _get_or_create_extension_instance(
|
|
|
23
23
|
return extension_instance
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def _normalize_path(model:
|
|
26
|
+
def _normalize_path(model: type["BaseModel"] | None, path: str) -> tuple[str, str]:
|
|
27
27
|
"""Resolve a path to (schema_urn, attribute_path)."""
|
|
28
28
|
from .resources.resource import Resource
|
|
29
29
|
|
|
30
30
|
# Absolute URN
|
|
31
31
|
if ":" in path:
|
|
32
|
+
if (
|
|
33
|
+
model
|
|
34
|
+
and issubclass(model, Resource)
|
|
35
|
+
and (
|
|
36
|
+
path in model.get_extension_models()
|
|
37
|
+
or path == model.model_fields["schemas"].default[0]
|
|
38
|
+
)
|
|
39
|
+
):
|
|
40
|
+
return path, ""
|
|
41
|
+
|
|
32
42
|
parts = path.rsplit(":", 1)
|
|
33
43
|
return parts[0], parts[1]
|
|
34
44
|
|
|
@@ -42,7 +52,7 @@ def _normalize_path(model: Optional[type["BaseModel"]], path: str) -> tuple[str,
|
|
|
42
52
|
|
|
43
53
|
def _validate_model_attribute(model: type["BaseModel"], attribute_base: str) -> None:
|
|
44
54
|
"""Validate that an attribute name or a sub-attribute path exist for a given model."""
|
|
45
|
-
attribute_name, *sub_attribute_blocks = attribute_base
|
|
55
|
+
attribute_name, *sub_attribute_blocks = _get_path_parts(attribute_base)
|
|
46
56
|
sub_attribute_base = ".".join(sub_attribute_blocks)
|
|
47
57
|
|
|
48
58
|
aliases = {field.validation_alias for field in model.model_fields.values()}
|
|
@@ -65,7 +75,7 @@ def _validate_model_attribute(model: type["BaseModel"], attribute_base: str) ->
|
|
|
65
75
|
|
|
66
76
|
def _validate_attribute_urn(
|
|
67
77
|
attribute_name: str, resource: type["Resource[Any]"]
|
|
68
|
-
) ->
|
|
78
|
+
) -> str | None:
|
|
69
79
|
"""Validate that an attribute urn is valid or not.
|
|
70
80
|
|
|
71
81
|
:param attribute_name: The attribute urn to check.
|
|
@@ -73,7 +83,7 @@ def _validate_attribute_urn(
|
|
|
73
83
|
"""
|
|
74
84
|
from .resources.resource import Resource
|
|
75
85
|
|
|
76
|
-
schema:
|
|
86
|
+
schema: Any | None
|
|
77
87
|
schema, attribute_base = _normalize_path(resource, attribute_name)
|
|
78
88
|
|
|
79
89
|
validated_resource = Resource.get_by_schema([resource], schema)
|
|
@@ -90,7 +100,7 @@ def _validate_attribute_urn(
|
|
|
90
100
|
|
|
91
101
|
def _resolve_path_to_target(
|
|
92
102
|
resource: "Resource[Any]", path: str
|
|
93
|
-
) -> tuple[
|
|
103
|
+
) -> tuple[Union["Resource[Any]", "Extension"] | None, str]:
|
|
94
104
|
"""Resolve a path to a target and an attribute_path.
|
|
95
105
|
|
|
96
106
|
The target can be the resource itself, or an extension object.
|
|
@@ -100,12 +110,17 @@ def _resolve_path_to_target(
|
|
|
100
110
|
if not schema_urn:
|
|
101
111
|
return resource, attr_path
|
|
102
112
|
|
|
113
|
+
if extension_class := resource.get_extension_model(schema_urn):
|
|
114
|
+
# Points to the extension root
|
|
115
|
+
if not attr_path:
|
|
116
|
+
return resource, extension_class.__name__
|
|
117
|
+
|
|
118
|
+
extension_instance = _get_or_create_extension_instance(
|
|
119
|
+
resource, extension_class
|
|
120
|
+
)
|
|
121
|
+
return extension_instance, attr_path
|
|
122
|
+
|
|
103
123
|
if schema_urn in resource.schemas:
|
|
104
124
|
return resource, attr_path
|
|
105
125
|
|
|
106
|
-
|
|
107
|
-
if not extension_class:
|
|
108
|
-
return (None, "")
|
|
109
|
-
|
|
110
|
-
extension_instance = _get_or_create_extension_instance(resource, extension_class)
|
|
111
|
-
return extension_instance, attr_path
|
|
126
|
+
return (None, "")
|
scim2_models/utils.py
CHANGED
|
@@ -3,7 +3,6 @@ import re
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
from typing import Annotated
|
|
5
5
|
from typing import Literal
|
|
6
|
-
from typing import Optional
|
|
7
6
|
from typing import Union
|
|
8
7
|
|
|
9
8
|
from pydantic import EncodedBytes
|
|
@@ -22,8 +21,12 @@ except ImportError:
|
|
|
22
21
|
# Python 3.9 has no UnionType
|
|
23
22
|
UNION_TYPES = [Union]
|
|
24
23
|
|
|
24
|
+
_UNDERSCORE_ALPHANUMERIC = re.compile(r"_+([0-9A-Za-z]+)")
|
|
25
|
+
_NON_WORD_UNDERSCORE = re.compile(r"[\W_]+")
|
|
26
|
+
_VALID_PATH_PATTERN = re.compile(r'^[a-zA-Z][a-zA-Z0-9._:\-\[\]"=\s]*$')
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
def _int_to_str(status: int | None) -> str | None:
|
|
27
30
|
return None if status is None else str(status)
|
|
28
31
|
|
|
29
32
|
|
|
@@ -87,7 +90,7 @@ def _to_camel(string: str) -> str:
|
|
|
87
90
|
'$ref' stays '$ref'.
|
|
88
91
|
"""
|
|
89
92
|
snake = to_snake(string)
|
|
90
|
-
camel =
|
|
93
|
+
camel = _UNDERSCORE_ALPHANUMERIC.sub(lambda m: m.group(1).title(), snake)
|
|
91
94
|
return camel
|
|
92
95
|
|
|
93
96
|
|
|
@@ -98,7 +101,7 @@ def _normalize_attribute_name(attribute_name: str) -> str:
|
|
|
98
101
|
"""
|
|
99
102
|
is_extension_attribute = ":" in attribute_name
|
|
100
103
|
if not is_extension_attribute:
|
|
101
|
-
attribute_name =
|
|
104
|
+
attribute_name = _NON_WORD_UNDERSCORE.sub("", attribute_name)
|
|
102
105
|
|
|
103
106
|
return attribute_name.lower()
|
|
104
107
|
|
|
@@ -122,7 +125,7 @@ def _validate_scim_path_syntax(path: str) -> bool:
|
|
|
122
125
|
|
|
123
126
|
# Cannot contain invalid characters (basic check)
|
|
124
127
|
# Allow alphanumeric, dots, underscores, hyphens, colons (for URNs), brackets
|
|
125
|
-
if not
|
|
128
|
+
if not _VALID_PATH_PATTERN.match(path):
|
|
126
129
|
return False
|
|
127
130
|
|
|
128
131
|
# If it contains a colon, validate it's a proper URN format
|
|
@@ -158,7 +161,7 @@ def _validate_scim_urn_syntax(path: str) -> bool:
|
|
|
158
161
|
return True
|
|
159
162
|
|
|
160
163
|
|
|
161
|
-
def _extract_field_name(path: str) ->
|
|
164
|
+
def _extract_field_name(path: str) -> str | None:
|
|
162
165
|
"""Extract the field name from a path.
|
|
163
166
|
|
|
164
167
|
For now, only handle simple paths (no filters, no complex expressions).
|
|
@@ -181,7 +184,7 @@ def _extract_field_name(path: str) -> Optional[str]:
|
|
|
181
184
|
return path
|
|
182
185
|
|
|
183
186
|
|
|
184
|
-
def _find_field_name(model_class: type["BaseModel"], attr_name: str) ->
|
|
187
|
+
def _find_field_name(model_class: type["BaseModel"], attr_name: str) -> str | None:
|
|
185
188
|
"""Find the actual field name in a resource class from an attribute name.
|
|
186
189
|
|
|
187
190
|
:param resource_class: The resource class to search in
|
|
@@ -195,3 +198,7 @@ def _find_field_name(model_class: type["BaseModel"], attr_name: str) -> Optional
|
|
|
195
198
|
return field_key
|
|
196
199
|
|
|
197
200
|
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _get_path_parts(path: str) -> list[str]:
|
|
204
|
+
return path.split(".")
|