Skip to content

Commit 1aac189

Browse files
committed
doc: improve flask integration documentation
1 parent dfd4492 commit 1aac189

File tree

4 files changed

+26
-32
lines changed

4 files changed

+26
-32
lines changed

doc/guides/_examples/fastapi_example.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,14 @@ def resource_location(request, app_record):
5252

5353

5454
# -- etag-start --
55-
def check_etag(record, if_match):
56-
"""Compare the record's ETag against an ``If-Match`` header value.
55+
def check_etag(record, request: Request):
56+
"""Compare the record's ETag against the ``If-Match`` request header.
5757
5858
:param record: The application record.
59-
:param if_match: Raw ``If-Match`` header value, or :data:`None`.
59+
:param request: The incoming request.
6060
:raises ~fastapi.HTTPException: If the header is present and does not match.
6161
"""
62+
if_match = request.headers.get("If-Match")
6263
if not if_match:
6364
return
6465
if if_match.strip() == "*":
@@ -138,7 +139,7 @@ async def get_user(request: Request, app_record: dict = Depends(resolve_user)):
138139
@router.patch("/Users/{user_id}")
139140
async def patch_user(request: Request, app_record: dict = Depends(resolve_user)):
140141
"""Apply a SCIM PatchOp to an existing user."""
141-
check_etag(app_record, request.headers.get("If-Match"))
142+
check_etag(app_record, request)
142143
scim_user = to_scim_user(app_record, resource_location(request, app_record))
143144
patch = PatchOp[User].model_validate(
144145
await request.json(),
@@ -162,7 +163,7 @@ async def patch_user(request: Request, app_record: dict = Depends(resolve_user))
162163
@router.put("/Users/{user_id}")
163164
async def replace_user(request: Request, app_record: dict = Depends(resolve_user)):
164165
"""Replace an existing user with a full SCIM resource."""
165-
check_etag(app_record, request.headers.get("If-Match"))
166+
check_etag(app_record, request)
166167
existing_user = to_scim_user(app_record, resource_location(request, app_record))
167168
replacement = User.model_validate(
168169
await request.json(),
@@ -190,7 +191,7 @@ async def replace_user(request: Request, app_record: dict = Depends(resolve_user
190191
@router.delete("/Users/{user_id}")
191192
async def delete_user(request: Request, app_record: dict = Depends(resolve_user)):
192193
"""Delete an existing user."""
193-
check_etag(app_record, request.headers.get("If-Match"))
194+
check_etag(app_record, request)
194195
delete_record(app_record["id"])
195196
return Response(status_code=HTTPStatus.NO_CONTENT)
196197

doc/guides/_examples/flask_example.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from flask import request
66
from flask import url_for
77
from pydantic import ValidationError
8+
from werkzeug.exceptions import HTTPException
89
from werkzeug.exceptions import PreconditionFailed
910
from werkzeug.routing import BaseConverter
1011
from werkzeug.routing import ValidationError as RoutingValidationError
@@ -51,13 +52,13 @@ def resource_location(app_record):
5152

5253

5354
# -- etag-start --
54-
def check_etag(record, if_match):
55-
"""Compare the record's ETag against an ``If-Match`` header value.
55+
def check_etag(record):
56+
"""Compare the record's ETag against the ``If-Match`` request header.
5657
5758
:param record: The application record.
58-
:param if_match: Raw ``If-Match`` header value, or :data:`None`.
5959
:raises ~werkzeug.exceptions.PreconditionFailed: If the header is present and does not match.
6060
"""
61+
if_match = request.headers.get("If-Match")
6162
if not if_match:
6263
return
6364
if if_match.strip() == "*":
@@ -100,11 +101,11 @@ def handle_validation_error(error):
100101
return scim_error.model_dump_json(), scim_error.status
101102

102103

103-
@bp.errorhandler(404)
104-
def handle_not_found(error):
105-
"""Turn Flask 404 errors into SCIM error responses."""
106-
scim_error = Error(status=404, detail=str(error.description))
107-
return scim_error.model_dump_json(), HTTPStatus.NOT_FOUND
104+
@bp.errorhandler(HTTPException)
105+
def handle_http_error(error):
106+
"""Turn HTTP errors into SCIM error responses."""
107+
scim_error = Error(status=error.code, detail=str(error.description))
108+
return scim_error.model_dump_json(), error.code
108109

109110

110111
@bp.errorhandler(SCIMException)
@@ -114,13 +115,6 @@ def handle_scim_error(error):
114115
return scim_error.model_dump_json(), scim_error.status
115116

116117

117-
@bp.errorhandler(PreconditionFailed)
118-
def handle_precondition_failed(error):
119-
"""Turn ETag mismatches into SCIM 412 responses."""
120-
scim_error = Error(status=412, detail="ETag mismatch")
121-
return scim_error.model_dump_json(), HTTPStatus.PRECONDITION_FAILED
122-
123-
124118
# -- error-handlers-end --
125119
# -- refinements-end --
126120

@@ -152,7 +146,7 @@ def get_user(app_record):
152146
@bp.patch("/Users/<user:app_record>")
153147
def patch_user(app_record):
154148
"""Apply a SCIM PatchOp to an existing user."""
155-
check_etag(app_record, request.headers.get("If-Match"))
149+
check_etag(app_record)
156150
scim_user = to_scim_user(app_record, resource_location(app_record))
157151
patch = PatchOp[User].model_validate(
158152
request.get_json(),
@@ -177,7 +171,7 @@ def patch_user(app_record):
177171
@bp.put("/Users/<user:app_record>")
178172
def replace_user(app_record):
179173
"""Replace an existing user with a full SCIM resource."""
180-
check_etag(app_record, request.headers.get("If-Match"))
174+
check_etag(app_record)
181175
existing_user = to_scim_user(app_record, resource_location(app_record))
182176
replacement = User.model_validate(
183177
request.get_json(),
@@ -204,7 +198,7 @@ def replace_user(app_record):
204198
@bp.delete("/Users/<user:app_record>")
205199
def delete_user(app_record):
206200
"""Delete an existing user."""
207-
check_etag(app_record, request.headers.get("If-Match"))
201+
check_etag(app_record)
208202
delete_record(app_record["id"])
209203
return "", HTTPStatus.NO_CONTENT
210204

doc/guides/fastapi.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ Resource versioning (ETags)
170170

171171
SCIM supports resource versioning through HTTP ETags
172172
(:rfc:`RFC 7644 §3.14 <7644#section-3.14>`).
173-
``check_etag`` compares the record's ETag against the ``If-Match`` header and
174-
raises an :class:`~fastapi.HTTPException` on mismatch.
173+
``check_etag`` reads the ``If-Match`` header from the incoming request, compares it against
174+
the record's ETag and raises an :class:`~fastapi.HTTPException` on mismatch.
175175
``make_etag`` computes a weak ETag from each record and populates
176176
:attr:`~scim2_models.Meta.version`.
177177

doc/guides/flask.rst

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ record and lets Flask handle the not-found case before entering the view.
5555
Error handlers
5656
^^^^^^^^^^^^^^
5757

58-
The error handlers keep Pydantic validation errors and Flask 404s aligned with SCIM
58+
The error handlers keep Pydantic validation errors and HTTP errors aligned with SCIM
5959
responses.
6060

6161
.. literalinclude:: _examples/flask_example.py
@@ -68,9 +68,8 @@ If :meth:`~scim2_models.Resource.model_validate`, Flask routes the
6868
SCIM :class:`~scim2_models.Error` response.
6969
``handle_scim_error`` catches any :class:`~scim2_models.SCIMException` (uniqueness, mutability, …)
7070
and returns the appropriate SCIM :class:`~scim2_models.Error` response.
71-
``handle_precondition_failed`` catches
72-
:class:`~werkzeug.exceptions.PreconditionFailed` errors raised by
73-
``check_etag`` and returns a 412.
71+
``handle_http_error`` catches any :class:`~werkzeug.exceptions.HTTPException`
72+
(404, 412, 405, …) and returns the corresponding SCIM :class:`~scim2_models.Error` response.
7473

7574
Endpoints
7675
=========
@@ -164,8 +163,8 @@ Resource versioning (ETags)
164163

165164
SCIM supports resource versioning through HTTP ETags
166165
(:rfc:`RFC 7644 §3.14 <7644#section-3.14>`).
167-
``check_etag`` compares the record's ETag against the ``If-Match`` header and
168-
raises :class:`~werkzeug.exceptions.PreconditionFailed` on mismatch.
166+
``check_etag`` reads the ``If-Match`` header from the current request, compares it against
167+
the record's ETag and raises :class:`~werkzeug.exceptions.PreconditionFailed` on mismatch.
169168
``make_etag`` computes a weak ETag from each record and populates
170169
:attr:`~scim2_models.Meta.version`.
171170

0 commit comments

Comments
 (0)