Skip to content

Commit f05c129

Browse files
committed
doc: document PUT in web integrations
1 parent ffb395b commit f05c129

File tree

6 files changed

+101
-7
lines changed

6 files changed

+101
-7
lines changed

doc/guides/_examples/django_example.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def handler404(request, exception):
8484
# -- single-resource-start --
8585
@method_decorator(csrf_exempt, name="dispatch")
8686
class UserView(View):
87-
"""Handle GET, PATCH and DELETE on one SCIM user resource."""
87+
"""Handle GET, PUT, PATCH and DELETE on one SCIM user resource."""
8888

8989
def get(self, request, app_record):
9090
try:
@@ -105,6 +105,31 @@ def delete(self, request, app_record):
105105
delete_record(app_record["id"])
106106
return scim_response("", HTTPStatus.NO_CONTENT)
107107

108+
def put(self, request, app_record):
109+
existing_user = to_scim_user(app_record)
110+
try:
111+
replacement = User.model_validate(
112+
json.loads(request.body),
113+
scim_ctx=Context.RESOURCE_REPLACEMENT_REQUEST,
114+
original=existing_user,
115+
)
116+
except ValidationError as error:
117+
return scim_validation_error(error)
118+
119+
replacement.id = existing_user.id
120+
updated_record = from_scim_user(replacement)
121+
try:
122+
save_record(updated_record)
123+
except ValueError as error:
124+
return scim_uniqueness_error(error)
125+
126+
response_user = to_scim_user(updated_record)
127+
return scim_response(
128+
response_user.model_dump_json(
129+
scim_ctx=Context.RESOURCE_REPLACEMENT_RESPONSE
130+
)
131+
)
132+
108133
def patch(self, request, app_record):
109134
try:
110135
patch = PatchOp[User].model_validate(

doc/guides/_examples/flask_example.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,31 @@ def patch_user(app_record):
119119
# -- patch-user-end --
120120

121121

122+
# -- put-user-start --
123+
@bp.put("/Users/<user:app_record>")
124+
def replace_user(app_record):
125+
"""Replace an existing user with a full SCIM resource."""
126+
existing_user = to_scim_user(app_record)
127+
replacement = User.model_validate(
128+
request.get_json(),
129+
scim_ctx=Context.RESOURCE_REPLACEMENT_REQUEST,
130+
original=existing_user,
131+
)
132+
133+
replacement.id = existing_user.id
134+
updated_record = from_scim_user(replacement)
135+
save_record(updated_record)
136+
137+
response_user = to_scim_user(updated_record)
138+
return (
139+
response_user.model_dump_json(
140+
scim_ctx=Context.RESOURCE_REPLACEMENT_RESPONSE
141+
),
142+
HTTPStatus.OK,
143+
)
144+
# -- put-user-end --
145+
146+
122147
# -- delete-user-start --
123148
@bp.delete("/Users/<user:app_record>")
124149
def delete_user(app_record):

doc/guides/django.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Django Integration
2-
------------------
1+
Django
2+
------
33

44
This guide shows a minimal SCIM integration with `Django <https://www.djangoproject.com/>`_
55
and :mod:`scim2_models`.
@@ -103,11 +103,16 @@ any other collection.
103103
Single resource
104104
^^^^^^^^^^^^^^^
105105

106-
``UserView`` handles ``GET``, ``PATCH`` and ``DELETE`` on ``/Users/<id>``.
106+
``UserView`` handles ``GET``, ``PUT``, ``PATCH`` and ``DELETE`` on ``/Users/<id>``.
107107
For ``GET``, parse query parameters with :class:`~scim2_models.ResponseParameters` to honour the
108108
``attributes`` and ``excludedAttributes`` query parameters, convert the native record to a
109109
SCIM resource, and serialize with :attr:`~scim2_models.Context.RESOURCE_QUERY_RESPONSE`.
110110
For ``DELETE``, remove the record and return an empty 204 response.
111+
For ``PUT``, validate the full replacement payload with
112+
:attr:`~scim2_models.Context.RESOURCE_REPLACEMENT_REQUEST`, passing the ``original`` resource
113+
so that immutable attributes are checked for unintended modifications.
114+
Convert back to native and persist, then serialize with
115+
:attr:`~scim2_models.Context.RESOURCE_REPLACEMENT_RESPONSE`.
111116
For ``PATCH``, validate the payload with :attr:`~scim2_models.Context.RESOURCE_PATCH_REQUEST`,
112117
apply it with :meth:`~scim2_models.PatchOp.patch` (generic, works with any resource type),
113118
convert back to native and persist, then serialize with

doc/guides/flask.rst

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Flask Integration
2-
-----------------
1+
Flask
2+
-----
33

44
This guide shows a minimal SCIM server built with `Flask <https://flask.palletsprojects.com/>`_
55
and :mod:`scim2_models`.
@@ -115,6 +115,20 @@ convert back to native and persist, then serialize the result with
115115
:start-after: # -- patch-user-start --
116116
:end-before: # -- patch-user-end --
117117

118+
PUT /Users/<id>
119+
^^^^^^^^^^^^^^^
120+
121+
Validate the full replacement payload with
122+
:attr:`~scim2_models.Context.RESOURCE_REPLACEMENT_REQUEST`, passing the ``original`` resource
123+
so that immutable attributes are checked for unintended modifications.
124+
Convert back to native and persist, then serialize the result with
125+
:attr:`~scim2_models.Context.RESOURCE_REPLACEMENT_RESPONSE`.
126+
127+
.. literalinclude:: _examples/flask_example.py
128+
:language: python
129+
:start-after: # -- put-user-start --
130+
:end-before: # -- put-user-end --
131+
118132

119133
GET /Users
120134
^^^^^^^^^^

doc/tutorial.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The standard Python dot notation uses snake_case attribute names, while the brac
1616
>>> user["nickName"] = "Babs"
1717
>>> user["name.familyName"] = "Jensen"
1818

19-
Attributes can be removed with ``del``.
19+
Attributes can be removed with ``del`` or by assigning :data:`None` to the attribute.
2020

2121
.. doctest::
2222

tests/test_doc_examples.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ def test_flask_example_smoke():
7878
assert duplicate_response.status_code == 409
7979
assert duplicate_response.get_json()["scimType"] == "uniqueness"
8080

81+
put_response = client.put(
82+
f"/scim/v2/Users/{user_id}",
83+
json={
84+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
85+
"userName": "bjensen@example.com",
86+
"displayName": "Barbara J.",
87+
},
88+
)
89+
assert put_response.status_code == 200
90+
assert put_response.get_json()["displayName"] == "Barbara J."
91+
8192

8293
def test_django_example_smoke():
8394
from django.conf import settings
@@ -166,3 +177,17 @@ def test_django_example_smoke():
166177
)
167178
assert duplicate_response.status_code == 409
168179
assert json.loads(duplicate_response.content)["scimType"] == "uniqueness"
180+
181+
put_response = client.put(
182+
f"/scim/v2/Users/{user_id}",
183+
data=json.dumps(
184+
{
185+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
186+
"userName": "bjensen@example.com",
187+
"displayName": "Barbara J.",
188+
}
189+
),
190+
content_type="application/scim+json",
191+
)
192+
assert put_response.status_code == 200
193+
assert json.loads(put_response.content)["displayName"] == "Barbara J."

0 commit comments

Comments
 (0)