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
16 changes: 16 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ Added
which is maintained, and live in ``scim2_client.engines.httpx2``.
They are shipped in the ``httpx2`` packaging extra.
`httpx <https://github.com/encode/httpx>`_ is still used when httpx2 is not installed.
- ``query``, ``delete`` and ``modify`` also accept a :class:`~scim2_models.Resource`
object in place of a resource type and an id. Objects without an id are rejected.
:issue:`13`
- ``replace``, ``modify`` and ``delete`` send an ``If-Match`` header when the server
advertises ETag support and the resource they are given carries a version.
:issue:`47`
- Resource versions are read from the ``ETag`` response header when the server does
not fill the ``meta.version`` attribute. :issue:`47`
- ``query`` sends an ``If-None-Match`` header when it is given a versioned resource
object and the server supports ETags. On a ``304 Not Modified`` answer, the object
that was passed is returned back. :issue:`47`
- ``409`` is an expected status code for ``delete``, as :rfc:`RFC7644 §3.12 <7644#section-3.12>`
defines it for every write operation.

Changed
^^^^^^^
Expand All @@ -20,6 +33,9 @@ Changed

Deprecated
^^^^^^^^^^
- The ``resource_model`` parameter of ``query``, ``delete`` and ``modify``, renamed
``target`` for ``query`` and ``resource`` for the two others, since it also accepts
resource objects. Will be removed in 0.9.
- The ``httpx`` packaging extra, in favor of the ``httpx2`` extra. Will be removed in 0.9.
- The ``scim2_client.engines.httpx`` module, in favor of ``scim2_client.engines.httpx2``.
Will be removed in 0.9.
Expand Down
144 changes: 141 additions & 3 deletions doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,10 @@ Create
Query
~~~~~

:meth:`~scim2_client.BaseSyncSCIMClient.query` issues a ``GET`` to read a single resource by its id, or list resources of a given type:
:meth:`~scim2_client.BaseSyncSCIMClient.query` issues a ``GET`` to read a single resource by its id, or list resources of a given type.

The resource to read is designated either by a resource type and an id, or by a
resource object carrying that id:

.. tab-set::
:class: outline
Expand All @@ -201,6 +204,7 @@ Query
from scim2_models import SearchRequest

user = scim.query(User, "my-user-id")
user = scim.query(User(id="my-user-id"))

response = scim.query(User, query_parameters=SearchRequest(filter='userName sw "john"'))
for user in response.resources:
Expand All @@ -214,6 +218,7 @@ Query
from scim2_models import SearchRequest

user = await scim.query(User, "my-user-id")
user = await scim.query(User(id="my-user-id"))

response = await scim.query(User, query_parameters=SearchRequest(filter='userName sw "john"'))
for user in response.resources:
Expand Down Expand Up @@ -282,13 +287,19 @@ Delete

scim.delete(User, "my-user-id")

user = scim.query(User, "my-user-id")
scim.delete(user)

.. tab-item:: Async
:sync: async

.. code-block:: python

await scim.delete(User, "my-user-id")

user = await scim.query(User, "my-user-id")
await scim.delete(user)

Modify
~~~~~~

Expand All @@ -308,7 +319,8 @@ Modify
PatchOperation(op=PatchOperation.Op.replace_, path="displayName", value="New Name"),
PatchOperation(op=PatchOperation.Op.add, path="emails", value=[{"value": "new@example.com"}]),
])
response = scim.modify(User, "my-user-id", patch)
user = scim.query(User, "my-user-id")
response = scim.modify(user, patch)

.. tab-item:: Async
:sync: async
Expand All @@ -321,7 +333,8 @@ Modify
PatchOperation(op=PatchOperation.Op.replace_, path="displayName", value="New Name"),
PatchOperation(op=PatchOperation.Op.add, path="emails", value=[{"value": "new@example.com"}]),
])
response = await scim.modify(User, "my-user-id", patch)
user = await scim.query(User, "my-user-id")
response = await scim.modify(user, patch)

Bulk
~~~~
Expand Down Expand Up @@ -398,6 +411,131 @@ To achieve this, all the methods provide the following parameters, all are :data
which value will excluded from the request payload, and which values are
expected in the response payload.

Resource versioning (ETags)
===========================

SCIM supports resource versioning through HTTP ETags
(:rfc:`RFC7644 §3.14 <7644#section-3.14>`).
When the server advertises ETag support in its
:class:`~scim2_models.ServiceProviderConfig`, scim2-client automatically makes
write operations conditional: :meth:`~scim2_client.BaseSyncSCIMClient.replace`,
:meth:`~scim2_client.BaseSyncSCIMClient.modify` and
:meth:`~scim2_client.BaseSyncSCIMClient.delete` send an ``If-Match`` header
built from the :attr:`meta.version <scim2_models.Meta.version>` of the resource
they are given.

This implements optimistic concurrency control: the server rejects the request
with a ``412 Precondition Failed`` error if the resource has been modified since
it was read.

.. note::

The client only knows about ETag support once it has read the
:class:`~scim2_models.ServiceProviderConfig`, either with
:meth:`~scim2_client.BaseSyncSCIMClient.discover` or by passing it to the
client :paramref:`~scim2_client.SCIMClient.service_provider_config`
parameter.

Conditional headers are only sent for resources the client has actually read,
since it is the server that fills the version. They are read from the
``ETag`` response header, or from the
:attr:`meta.version <scim2_models.Meta.version>` attribute when the server
fills it.

.. tab-set::
:class: outline

.. tab-item:: Sync
:sync: sync

.. code-block:: python

from scim2_models import SCIMException

scim.discover()

# The version is read from the server response
user = scim.query(User, "my-user-id")

# If-Match is sent automatically
user.display_name = "Updated Name"
try:
user = scim.replace(user)
except SCIMException as exc:
if exc.status == 412:
print("The resource has changed, read it again")
else:
raise

# If-Match is sent automatically here too
scim.delete(user)

.. tab-item:: Async
:sync: async

.. code-block:: python

from scim2_models import SCIMException

await scim.discover()

# The version is read from the server response
user = await scim.query(User, "my-user-id")

# If-Match is sent automatically
user.display_name = "Updated Name"
try:
user = await scim.replace(user)
except SCIMException as exc:
if exc.status == 412:
print("The resource has changed, read it again")
else:
raise

# If-Match is sent automatically here too
await scim.delete(user)

Reads are conditional too: :meth:`~scim2_client.BaseSyncSCIMClient.query` sends
an ``If-None-Match`` header when it is given a versioned resource object. When
the server answers with a ``304 Not Modified``, nothing is downloaded and the
object that was passed is returned back:

.. tab-set::
:class: outline

.. tab-item:: Sync
:sync: sync

.. code-block:: python

user = scim.query(User, "my-user-id")

# If-None-Match is sent; 'fresh' is 'user' itself on a 304
fresh = scim.query(user)

.. tab-item:: Async
:sync: async

.. code-block:: python

user = await scim.query(User, "my-user-id")

# If-None-Match is sent; 'fresh' is 'user' itself on a 304
fresh = await scim.query(user)

.. warning::

On a ``304 Not Modified`` the very object that was passed is returned, not a
copy. Local modifications made to it are therefore given back as if they came
from the server.

No ``If-None-Match`` is sent when ``query_parameters`` are used, since the server
would then answer with a partial representation that the cached object cannot
stand for.

No additional configuration is needed. When the server does not advertise ETag
support, or when the resource carries no version, no conditional header is sent.

Engines
=======

Expand Down
Loading
Loading