scim2-server 0.7.0__tar.gz → 0.7.1__tar.gz

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.
Files changed (30) hide show
  1. {scim2_server-0.7.0 → scim2_server-0.7.1}/PKG-INFO +2 -2
  2. {scim2_server-0.7.0 → scim2_server-0.7.1}/pyproject.toml +3 -3
  3. {scim2_server-0.7.0 → scim2_server-0.7.1}/pyproject.toml.orig +3 -3
  4. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/handler.py +7 -2
  5. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/memory.py +21 -3
  6. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/storage.py +71 -2
  7. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testing.py +63 -2
  8. {scim2_server-0.7.0 → scim2_server-0.7.1}/LICENSE +0 -0
  9. {scim2_server-0.7.0 → scim2_server-0.7.1}/README.md +0 -0
  10. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/__init__.py +0 -0
  11. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/applications/__init__.py +0 -0
  12. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/applications/asgi.py +0 -0
  13. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/applications/base.py +0 -0
  14. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/applications/wsgi.py +0 -0
  15. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/bulk.py +0 -0
  16. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/conditions.py +0 -0
  17. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/errors.py +0 -0
  18. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/py.typed +0 -0
  19. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/requests.py +0 -0
  20. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/resources/default-resource-types.json +0 -0
  21. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/resources/default-schemas.json +0 -0
  22. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/resources/default-service-provider-config.json +0 -0
  23. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/responses.py +0 -0
  24. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/routing.py +0 -0
  25. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/service.py +0 -0
  26. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testserver/__init__.py +0 -0
  27. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testserver/application.py +0 -0
  28. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testserver/cli.py +0 -0
  29. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/testserver/precis.py +0 -0
  30. {scim2_server-0.7.0 → scim2_server-0.7.1}/scim2_server/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scim2-server
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: SCIM server core for any storage and web framework
5
5
  Keywords: scim,scim2,provisioning,rfc7643,rfc7644
6
6
  Author: Yaal Coop, Christian Friedrich Coors
@@ -217,7 +217,7 @@ Classifier: License :: OSI Approved :: Apache Software License
217
217
  Classifier: Environment :: Web Environment
218
218
  Classifier: Programming Language :: Python
219
219
  Classifier: Operating System :: OS Independent
220
- Requires-Dist: scim2-models>=0.12.0
220
+ Requires-Dist: scim2-models>=0.12.1
221
221
  Requires-Dist: precis-i18n>=1.1.2 ; extra == 'precis'
222
222
  Requires-Dist: pytest>=9.1.1 ; extra == 'testing'
223
223
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-server"
7
- version = "0.7.0"
7
+ version = "0.7.1"
8
8
  description = "SCIM server core for any storage and web framework"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -28,7 +28,7 @@ classifiers = [
28
28
  "Operating System :: OS Independent",
29
29
  ]
30
30
  requires-python = ">= 3.11"
31
- dependencies = ["scim2-models>=0.12.0"]
31
+ dependencies = ["scim2-models>=0.12.1"]
32
32
 
33
33
  [[project.authors]]
34
34
  name = "Yaal Coop"
@@ -63,7 +63,7 @@ dev = [
63
63
  "aiosqlite>=0.22.1",
64
64
  "precis-i18n>=1.1.2",
65
65
  "mypy>=2.3.1",
66
- "scim2-client[werkzeug]>=0.11.2",
66
+ "scim2-client>=0.12.0",
67
67
  "scim2-tester>=0.5.1",
68
68
  "werkzeug>=3.0.3",
69
69
  ]
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-server"
7
- version = "0.7.0"
7
+ version = "0.7.1"
8
8
  description = "SCIM server core for any storage and web framework"
9
9
  authors = [
10
10
  {name="Yaal Coop", email="contact@yaal.coop"},
@@ -29,7 +29,7 @@ classifiers = [
29
29
 
30
30
  requires-python = ">= 3.11"
31
31
  dependencies = [
32
- "scim2-models>=0.12.0",
32
+ "scim2-models>=0.12.1",
33
33
  ]
34
34
 
35
35
  [project.optional-dependencies]
@@ -58,7 +58,7 @@ dev = [
58
58
  "aiosqlite>=0.22.1",
59
59
  "precis-i18n>=1.1.2",
60
60
  "mypy>=2.3.1",
61
- "scim2-client[werkzeug]>=0.11.2",
61
+ "scim2-client>=0.12.0",
62
62
  "scim2-tester>=0.5.1",
63
63
  "werkzeug>=3.0.3",
64
64
  ]
@@ -15,6 +15,7 @@ from scim2_server.routing import Operation
15
15
  from scim2_server.service import ScimService
16
16
  from scim2_server.storage import AsyncScimStorage
17
17
  from scim2_server.storage import ScimStorage
18
+ from scim2_server.storage import projection
18
19
 
19
20
 
20
21
  class ScimHandler:
@@ -75,7 +76,9 @@ class ScimHandler:
75
76
  )
76
77
  with self.storage.operation():
77
78
  resource = self.storage.get(
78
- resource_type, cast(str, target.resource_id)
79
+ resource_type,
80
+ cast(str, target.resource_id),
81
+ **projection(self.storage, response_parameters),
79
82
  )
80
83
  response = self.service.query_response(
81
84
  request.base_url,
@@ -398,7 +401,9 @@ class AsyncScimHandler:
398
401
  )
399
402
  async with self.storage.operation():
400
403
  resource = await self.storage.get(
401
- resource_type, cast(str, target.resource_id)
404
+ resource_type,
405
+ cast(str, target.resource_id),
406
+ **projection(self.storage, response_parameters),
402
407
  )
403
408
  response = self.service.query_response(
404
409
  request.base_url,
@@ -16,6 +16,7 @@ from scim2_models import Path
16
16
  from scim2_models import PreconditionFailedException
17
17
  from scim2_models import Resource
18
18
  from scim2_models import ResourceType
19
+ from scim2_models import ResponseParameters
19
20
  from scim2_models import ScimFilter
20
21
  from scim2_models import SearchRequest
21
22
  from scim2_models import Uniqueness
@@ -23,6 +24,7 @@ from scim2_models import UniquenessException
23
24
 
24
25
  from scim2_server.storage import AsyncScimStorage
25
26
  from scim2_server.storage import ScimStorage
27
+ from scim2_server.storage import projection
26
28
  from scim2_server.utils import parametrize
27
29
 
28
30
 
@@ -68,7 +70,13 @@ class InMemoryStorage(ScimStorage):
68
70
  with self.lock:
69
71
  yield
70
72
 
71
- def get(self, resource_type: ResourceType, resource_id: str) -> Resource[Any]:
73
+ def get(
74
+ self,
75
+ resource_type: ResourceType,
76
+ resource_id: str,
77
+ *,
78
+ response_parameters: ResponseParameters[Any] | None = None,
79
+ ) -> Resource[Any]:
72
80
  with self.lock:
73
81
  return self.resources[self._index(resource_type, resource_id)].model_copy(
74
82
  deep=True
@@ -236,8 +244,18 @@ class AsyncInMemoryStorage(AsyncScimStorage):
236
244
  """The stored resources."""
237
245
  return self.storage.resources
238
246
 
239
- async def get(self, resource_type: ResourceType, resource_id: str) -> Resource[Any]:
240
- return self.storage.get(resource_type, resource_id)
247
+ async def get(
248
+ self,
249
+ resource_type: ResourceType,
250
+ resource_id: str,
251
+ *,
252
+ response_parameters: ResponseParameters[Any] | None = None,
253
+ ) -> Resource[Any]:
254
+ return self.storage.get(
255
+ resource_type,
256
+ resource_id,
257
+ **projection(self.storage, response_parameters),
258
+ )
241
259
 
242
260
  async def search(
243
261
  self, resource_types: list[ResourceType], search_request: SearchRequest[Any]
@@ -1,3 +1,5 @@
1
+ import inspect
2
+ import warnings
1
3
  from abc import ABC
2
4
  from abc import abstractmethod
3
5
  from contextlib import AbstractAsyncContextManager
@@ -7,9 +9,39 @@ from typing import Any
7
9
 
8
10
  from scim2_models import Resource
9
11
  from scim2_models import ResourceType
12
+ from scim2_models import ResponseParameters
10
13
  from scim2_models import SearchRequest
11
14
 
12
15
 
16
+ def takes_response_parameters(storage_class: type) -> bool:
17
+ """Tell whether the ``get`` method of a storage accepts ``response_parameters``, and warn when it does not."""
18
+ parameters = inspect.signature(storage_class.get).parameters.values() # type: ignore[attr-defined]
19
+ if any(
20
+ parameter.name == "response_parameters"
21
+ or parameter.kind is inspect.Parameter.VAR_KEYWORD
22
+ for parameter in parameters
23
+ ):
24
+ return True
25
+ warnings.warn(
26
+ f"{storage_class.__qualname__}.get should accept a response_parameters "
27
+ "keyword argument. scim2-server 0.9 will require it.",
28
+ DeprecationWarning,
29
+ # takes_response_parameters, __init_subclass__, ABCMeta.__new__, then the class statement
30
+ stacklevel=4,
31
+ )
32
+ return False
33
+
34
+
35
+ def projection(
36
+ storage: "ScimStorage | AsyncScimStorage",
37
+ response_parameters: ResponseParameters[Any] | None,
38
+ ) -> dict[str, Any]:
39
+ """Return the arguments that pass the response parameters to the get method of a storage, if it accepts them."""
40
+ if not storage._get_takes_response_parameters:
41
+ return {}
42
+ return {"response_parameters": response_parameters}
43
+
44
+
13
45
  class ScimStorage(ABC):
14
46
  """Where a SCIM server reads and writes its resources.
15
47
 
@@ -40,10 +72,35 @@ class ScimStorage(ABC):
40
72
  calling the storage.
41
73
  """
42
74
 
75
+ _get_takes_response_parameters = True
76
+
77
+ def __init_subclass__(cls, **kwargs: Any) -> None:
78
+ super().__init_subclass__(**kwargs)
79
+ cls._get_takes_response_parameters = takes_response_parameters(cls)
80
+
43
81
  @abstractmethod
44
- def get(self, resource_type: ResourceType, resource_id: str) -> Resource[Any]:
82
+ def get(
83
+ self,
84
+ resource_type: ResourceType,
85
+ resource_id: str,
86
+ *,
87
+ response_parameters: ResponseParameters[Any] | None = None,
88
+ ) -> Resource[Any]:
45
89
  """Return a resource.
46
90
 
91
+ The server passes ``response_parameters`` when it returns the resource
92
+ to the client, for a GET request. The storage may then leave out the
93
+ attributes the response does not keep, for instance to avoid loading
94
+ the members of a group. It must return every attribute the response
95
+ keeps. :meth:`Path.iter_paths <scim2_models.Path.iter_paths>` gives
96
+ them. The server applies the parameters to the response anyway.
97
+ Without ``response_parameters``, the storage returns the whole
98
+ resource.
99
+
100
+ A ``get`` method without ``response_parameters`` is deprecated, and
101
+ raises a :class:`DeprecationWarning`. The server then does not pass
102
+ the parameters. scim2-server 0.9 will require it.
103
+
47
104
  :raises ~scim2_models.NotFoundException: When no resource of this type
48
105
  has this identifier.
49
106
  """
@@ -135,8 +192,20 @@ class AsyncScimStorage(ABC):
135
192
  which :class:`~scim2_server.testing.AsyncScimStorageContract` checks.
136
193
  """
137
194
 
195
+ _get_takes_response_parameters = True
196
+
197
+ def __init_subclass__(cls, **kwargs: Any) -> None:
198
+ super().__init_subclass__(**kwargs)
199
+ cls._get_takes_response_parameters = takes_response_parameters(cls)
200
+
138
201
  @abstractmethod
139
- async def get(self, resource_type: ResourceType, resource_id: str) -> Resource[Any]:
202
+ async def get(
203
+ self,
204
+ resource_type: ResourceType,
205
+ resource_id: str,
206
+ *,
207
+ response_parameters: ResponseParameters[Any] | None = None,
208
+ ) -> Resource[Any]:
140
209
  """Return a resource. See :meth:`ScimStorage.get`."""
141
210
 
142
211
  @abstractmethod
@@ -29,16 +29,19 @@ from typing import Union
29
29
  from typing import cast
30
30
 
31
31
  import pytest
32
+ from scim2_models import Context
32
33
  from scim2_models import NotFoundException
33
34
  from scim2_models import PreconditionFailedException
34
35
  from scim2_models import Resource
35
36
  from scim2_models import ResourceType
37
+ from scim2_models import ResponseParameters
36
38
  from scim2_models import ScimProvider
37
39
  from scim2_models import SearchRequest
38
40
  from scim2_models import UniquenessException
39
41
 
40
42
  from scim2_server.storage import AsyncScimStorage
41
43
  from scim2_server.storage import ScimStorage
44
+ from scim2_server.storage import projection
42
45
  from scim2_server.utils import load_default_provider
43
46
  from scim2_server.utils import parametrize
44
47
 
@@ -198,6 +201,52 @@ class ScimStorageContract:
198
201
  with pytest.raises(NotFoundException):
199
202
  storage.get(group_type, user.id)
200
203
 
204
+ @pytest.mark.parametrize(
205
+ "parameters",
206
+ [
207
+ {"excludedAttributes": "members"},
208
+ {"excludedAttributes": "members.display"},
209
+ {"attributes": "displayName"},
210
+ {"attributes": "members.value"},
211
+ ],
212
+ )
213
+ def test_get_keeps_what_the_response_returns(
214
+ self,
215
+ storage: Any,
216
+ user_type: ResourceType,
217
+ group_type: ResourceType,
218
+ user_model: Any,
219
+ group_model: Any,
220
+ parameters: dict[str, str],
221
+ ) -> None:
222
+ """A resource read with response parameters gives the same response as the whole resource."""
223
+ (user,) = self.create_users(storage, user_type, user_model, "bjensen")
224
+ group = storage.create(
225
+ group_type,
226
+ group_model.model_validate(
227
+ {
228
+ "displayName": "Admins",
229
+ "members": [{"value": user.id, "display": "bjensen"}],
230
+ }
231
+ ),
232
+ )
233
+ response_parameters = parametrize(
234
+ ResponseParameters, group_model
235
+ ).model_validate(parameters)
236
+
237
+ whole = storage.get(group_type, group.id)
238
+ projected = storage.get(
239
+ group_type, group.id, **projection(storage, response_parameters)
240
+ )
241
+
242
+ assert projected.model_dump(
243
+ scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
244
+ response_parameters=response_parameters,
245
+ ) == whole.model_dump(
246
+ scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
247
+ response_parameters=response_parameters,
248
+ )
249
+
201
250
  def test_update_replaces_the_stored_resource(
202
251
  self, storage: Any, user_type: ResourceType, user_model: Any
203
252
  ) -> None:
@@ -492,8 +541,20 @@ class BlockingStorage(ScimStorage):
492
541
  def run(self, coroutine: Coroutine[Any, Any, T]) -> T:
493
542
  return self.runner.run(coroutine)
494
543
 
495
- def get(self, resource_type: ResourceType, resource_id: str) -> Resource[Any]:
496
- return self.run(self.storage.get(resource_type, resource_id))
544
+ def get(
545
+ self,
546
+ resource_type: ResourceType,
547
+ resource_id: str,
548
+ *,
549
+ response_parameters: ResponseParameters[Any] | None = None,
550
+ ) -> Resource[Any]:
551
+ return self.run(
552
+ self.storage.get(
553
+ resource_type,
554
+ resource_id,
555
+ **projection(self.storage, response_parameters),
556
+ )
557
+ )
497
558
 
498
559
  def search(
499
560
  self, resource_types: list[ResourceType], search_request: SearchRequest[Any]
File without changes
File without changes