scim2-models 0.2.9__tar.gz → 0.2.11__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 (109) hide show
  1. {scim2_models-0.2.9 → scim2_models-0.2.11}/PKG-INFO +1 -1
  2. {scim2_models-0.2.9 → scim2_models-0.2.11}/doc/changelog.rst +19 -0
  3. {scim2_models-0.2.9 → scim2_models-0.2.11}/pyproject.toml +1 -1
  4. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.2-user-full.json +1 -1
  5. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.3-enterprise_user.json +1 -1
  6. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7643/enterprise_user.py +3 -1
  7. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7643/group.py +4 -1
  8. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7643/resource.py +2 -2
  9. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7643/resource_type.py +3 -1
  10. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7643/schema.py +12 -3
  11. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7643/service_provider_config.py +3 -1
  12. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7643/user.py +4 -2
  13. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7644/bulk.py +7 -2
  14. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7644/error.py +4 -1
  15. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7644/list_response.py +4 -1
  16. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7644/message.py +4 -1
  17. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7644/patch_op.py +5 -1
  18. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7644/search_request.py +19 -1
  19. scim2_models-0.2.11/scim2_models/utils.py +99 -0
  20. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_dynamic_resources.py +1 -2
  21. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_list_response.py +3 -1
  22. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_model_attributes.py +13 -11
  23. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_model_serialization.py +3 -2
  24. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_model_validation.py +3 -3
  25. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_resource_extension.py +3 -1
  26. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_schema.py +13 -0
  27. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_search_request.py +6 -0
  28. {scim2_models-0.2.9 → scim2_models-0.2.11}/uv.lock +93 -93
  29. scim2_models-0.2.9/scim2_models/utils.py +0 -42
  30. {scim2_models-0.2.9 → scim2_models-0.2.11}/.github/FUNDING.yml +0 -0
  31. {scim2_models-0.2.9 → scim2_models-0.2.11}/.github/workflows/release.yml +0 -0
  32. {scim2_models-0.2.9 → scim2_models-0.2.11}/.github/workflows/tests.yaml +0 -0
  33. {scim2_models-0.2.9 → scim2_models-0.2.11}/.gitignore +0 -0
  34. {scim2_models-0.2.9 → scim2_models-0.2.11}/.pre-commit-config.yaml +0 -0
  35. {scim2_models-0.2.9 → scim2_models-0.2.11}/.readthedocs.yml +0 -0
  36. {scim2_models-0.2.9 → scim2_models-0.2.11}/LICENSE +0 -0
  37. {scim2_models-0.2.9 → scim2_models-0.2.11}/README.md +0 -0
  38. {scim2_models-0.2.9 → scim2_models-0.2.11}/conftest.py +0 -0
  39. {scim2_models-0.2.9 → scim2_models-0.2.11}/doc/__init__.py +0 -0
  40. {scim2_models-0.2.9 → scim2_models-0.2.11}/doc/conf.py +0 -0
  41. {scim2_models-0.2.9 → scim2_models-0.2.11}/doc/contributing.rst +0 -0
  42. {scim2_models-0.2.9 → scim2_models-0.2.11}/doc/index.rst +0 -0
  43. {scim2_models-0.2.9 → scim2_models-0.2.11}/doc/reference.rst +0 -0
  44. {scim2_models-0.2.9 → scim2_models-0.2.11}/doc/tutorial.rst +0 -0
  45. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.1-user-minimal.json +0 -0
  46. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.4-group.json +0 -0
  47. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.5-service_provider_configuration.json +0 -0
  48. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.6-resource_type-group.json +0 -0
  49. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.6-resource_type-user.json +0 -0
  50. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.7.1-schema-enterprise_user.json +0 -0
  51. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.7.1-schema-group.json +0 -0
  52. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.7.1-schema-user.json +0 -0
  53. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.7.2-schema-resource_type.json +0 -0
  54. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.7.2-schema-schema.json +0 -0
  55. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7643-8.7.2-schema-service_provider_configuration.json +0 -0
  56. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.12-error-bad_request.json +0 -0
  57. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.12-error-not_found.json +0 -0
  58. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.14-user-post_request.json +0 -0
  59. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.14-user-post_response.json +0 -0
  60. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.3-user-post_request.json +0 -0
  61. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.3-user-post_response.json +0 -0
  62. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.4.1-user-known-resource.json +0 -0
  63. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.4.2-list_response-partial_attributes.json +0 -0
  64. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.4.3-list_response-post_query.json +0 -0
  65. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.4.3-search_request.json +0 -0
  66. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.1-user-put_request.json +0 -0
  67. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.1-user-put_response.json +0 -0
  68. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.1-patch_op-add_emails.json +0 -0
  69. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.1-patch_op-add_members.json +0 -0
  70. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.2-patch_op-remove_all_members.json +0 -0
  71. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.2-patch_op-remove_and_add_one_member.json +0 -0
  72. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.2-patch_op-remove_multi_complex_value.json +0 -0
  73. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.2-patch_op-remove_one_member.json +0 -0
  74. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.3-patch_op-replace_all_email_values.json +0 -0
  75. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.3-patch_op-replace_all_members.json +0 -0
  76. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.3-patch_op-replace_street_address.json +0 -0
  77. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.5.2.3-patch_op-replace_user_work_address.json +0 -0
  78. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.6-error-not_found.json +0 -0
  79. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.1-bulk_request-circular_conflict.json +0 -0
  80. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.1-list_response-circular_reference.json +0 -0
  81. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.2-bulk_request-enterprise_user.json +0 -0
  82. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.2-bulk_request-temporary_identifier.json +0 -0
  83. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.2-bulk_response-temporary_identifier.json +0 -0
  84. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.3-bulk_request-multiple_operations.json +0 -0
  85. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.3-bulk_response-error_invalid_syntax.json +0 -0
  86. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.3-bulk_response-multiple_errors.json +0 -0
  87. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.3-bulk_response-multiple_operations.json +0 -0
  88. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.3-error-invalid_syntax.json +0 -0
  89. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.7.4-error-payload_too_large.json +0 -0
  90. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-3.9-user-partial_response.json +0 -0
  91. {scim2_models-0.2.9 → scim2_models-0.2.11}/samples/rfc7644-4-list_response-resource_types.json +0 -0
  92. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/__init__.py +0 -0
  93. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/base.py +0 -0
  94. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/constants.py +0 -0
  95. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/py.typed +0 -0
  96. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7643/__init__.py +0 -0
  97. {scim2_models-0.2.9 → scim2_models-0.2.11}/scim2_models/rfc7644/__init__.py +0 -0
  98. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/__init__.py +0 -0
  99. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/conftest.py +0 -0
  100. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_dynamic_schemas.py +0 -0
  101. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_enterprise_user.py +0 -0
  102. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_errors.py +0 -0
  103. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_group.py +0 -0
  104. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_models.py +0 -0
  105. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_patch_op.py +0 -0
  106. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_resource_type.py +0 -0
  107. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_service_provider_configuration.py +0 -0
  108. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_user.py +0 -0
  109. {scim2_models-0.2.9 → scim2_models-0.2.11}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scim2-models
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: SCIM2 models serialization and validation with pydantic
5
5
  Project-URL: documentation, https://scim2-models.readthedocs.io
6
6
  Project-URL: repository, https://github.com/python-scim/scim2-models
@@ -1,6 +1,25 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ [0.2.11] - 2024-12-08
5
+ ---------------------
6
+ Added
7
+ ^^^^^
8
+ - Implement :meth:`Schema.get_attribute <scim2_models.Schema.get_attribute>`.
9
+ - Implement :meth:`SearchRequest.start_index_0 <scim2_models.SearchRequest.start_index_0>`
10
+ and :meth:`SearchRequest.start_index_1 <scim2_models.SearchRequest.start_index_1>`.
11
+
12
+ [0.2.10] - 2024-12-02
13
+ ---------------------
14
+
15
+ Changed
16
+ ^^^^^^^
17
+ - The ``schema`` attribute is annotated with :attr:`~scim2_models.Required.true`.
18
+
19
+ Fixed
20
+ ^^^^^
21
+ - ``Base64Bytes`` compatibility between pydantic 2.10+ and <2.10
22
+
4
23
  [0.2.9] - 2024-12-02
5
24
  --------------------
6
25
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-models"
7
- version = "0.2.9"
7
+ version = "0.2.11"
8
8
  description = "SCIM2 models serialization and validation with pydantic"
9
9
  authors = [{name="Yaal Coop", email="contact@yaal.coop"}]
10
10
  license = {file = "LICENSE"}
@@ -100,7 +100,7 @@
100
100
  ],
101
101
  "x509Certificates": [
102
102
  {
103
- "value": "MIIDQzCCAqygAwIBAgICEAAwDQYJKoZIhvcNAQEFBQAwTjELMAkGA1UEBhMCVVMxEzARBgNVBAgM\nCkNhbGlmb3JuaWExFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAe\nFw0xMTEwMjIwNjI0MzFaFw0xMjEwMDQwNjI0MzFaMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD\nYWxpZm9ybmlhMRQwEgYDVQQKDAtleGFtcGxlLmNvbTEhMB8GA1UEAwwYTXMuIEJhcmJhcmEgSiBK\nZW5zZW4gSUlJMSIwIAYJKoZIhvcNAQkBFhNiamVuc2VuQGV4YW1wbGUuY29tMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Kr+Dcds/JQ5GwejJFcBIP682X3xpjis56AK02bc1FLgzdLI\n8auoR+cC9/Vrh5t66HkQIOdA4unHh0AaZ4xL5PhVbXIPMB5vAPKpzz5iPSi8xO8SL7I7SDhcBVJh\nqVqr3HgllEG6UClDdHO7nkLuwXq8HcISKkbT5WFTVfFZzidPl8HZ7DhXkZIRtJwBweq4bvm3hM1O\ns7UQH05ZS6cVDgweKNwdLLrT51ikSQG3DYrl+ft781UQRIqxgwqCfXEuDiinPh0kkvIi5jivVu1Z\n9QiwlYEdRbLJ4zJQBmDrSGTMYn4lRc2HgHO4DqB/bnMVorHB0CC6AV1QoFK4GPe1LwIDAQABo3sw\neTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0\nZTAdBgNVHQ4EFgQU8pD0U0vsZIsaA16lL8En8bx0F/gwHwYDVR0jBBgwFoAUdGeKitcaF7gnzsNw\nDx708kqaVt0wDQYJKoZIhvcNAQEFBQADgYEAA81SsFnOdYJtNg5Tcq+/ByEDrBgnusx0jloUhByP\nMEVkoMZ3J7j1ZgI8rAbOkNngX8+pKfTiDz1RC4+dx8oU6Za+4NJXUjlL5CvV6BEYb1+QAEJwitTV\nvxB/A67g42/vzgAtoRUeDov1+GFiBZ+GNF/cAYKcMtGcrs2i97ZkJMo=\n"
103
+ "value": "MIIDQzCCAqygAwIBAgICEAAwDQYJKoZIhvcNAQEFBQAwTjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xMTEwMjIwNjI0MzFaFw0xMjEwMDQwNjI0MzFaMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQKDAtleGFtcGxlLmNvbTEhMB8GA1UEAwwYTXMuIEJhcmJhcmEgSiBKZW5zZW4gSUlJMSIwIAYJKoZIhvcNAQkBFhNiamVuc2VuQGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Kr+Dcds/JQ5GwejJFcBIP682X3xpjis56AK02bc1FLgzdLI8auoR+cC9/Vrh5t66HkQIOdA4unHh0AaZ4xL5PhVbXIPMB5vAPKpzz5iPSi8xO8SL7I7SDhcBVJhqVqr3HgllEG6UClDdHO7nkLuwXq8HcISKkbT5WFTVfFZzidPl8HZ7DhXkZIRtJwBweq4bvm3hM1Os7UQH05ZS6cVDgweKNwdLLrT51ikSQG3DYrl+ft781UQRIqxgwqCfXEuDiinPh0kkvIi5jivVu1Z9QiwlYEdRbLJ4zJQBmDrSGTMYn4lRc2HgHO4DqB/bnMVorHB0CC6AV1QoFK4GPe1LwIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU8pD0U0vsZIsaA16lL8En8bx0F/gwHwYDVR0jBBgwFoAUdGeKitcaF7gnzsNwDx708kqaVt0wDQYJKoZIhvcNAQEFBQADgYEAA81SsFnOdYJtNg5Tcq+/ByEDrBgnusx0jloUhByPMEVkoMZ3J7j1ZgI8rAbOkNngX8+pKfTiDz1RC4+dx8oU6Za+4NJXUjlL5CvV6BEYb1+QAEJwitTVvxB/A67g42/vzgAtoRUeDov1+GFiBZ+GNF/cAYKcMtGcrs2i97ZkJMo="
104
104
  }
105
105
  ],
106
106
  "meta": {
@@ -101,7 +101,7 @@
101
101
  ],
102
102
  "x509Certificates": [
103
103
  {
104
- "value": "MIIDQzCCAqygAwIBAgICEAAwDQYJKoZIhvcNAQEFBQAwTjELMAkGA1UEBhMCVVMxEzARBgNVBAgM\nCkNhbGlmb3JuaWExFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAe\nFw0xMTEwMjIwNjI0MzFaFw0xMjEwMDQwNjI0MzFaMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD\nYWxpZm9ybmlhMRQwEgYDVQQKDAtleGFtcGxlLmNvbTEhMB8GA1UEAwwYTXMuIEJhcmJhcmEgSiBK\nZW5zZW4gSUlJMSIwIAYJKoZIhvcNAQkBFhNiamVuc2VuQGV4YW1wbGUuY29tMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Kr+Dcds/JQ5GwejJFcBIP682X3xpjis56AK02bc1FLgzdLI\n8auoR+cC9/Vrh5t66HkQIOdA4unHh0AaZ4xL5PhVbXIPMB5vAPKpzz5iPSi8xO8SL7I7SDhcBVJh\nqVqr3HgllEG6UClDdHO7nkLuwXq8HcISKkbT5WFTVfFZzidPl8HZ7DhXkZIRtJwBweq4bvm3hM1O\ns7UQH05ZS6cVDgweKNwdLLrT51ikSQG3DYrl+ft781UQRIqxgwqCfXEuDiinPh0kkvIi5jivVu1Z\n9QiwlYEdRbLJ4zJQBmDrSGTMYn4lRc2HgHO4DqB/bnMVorHB0CC6AV1QoFK4GPe1LwIDAQABo3sw\neTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0\nZTAdBgNVHQ4EFgQU8pD0U0vsZIsaA16lL8En8bx0F/gwHwYDVR0jBBgwFoAUdGeKitcaF7gnzsNw\nDx708kqaVt0wDQYJKoZIhvcNAQEFBQADgYEAA81SsFnOdYJtNg5Tcq+/ByEDrBgnusx0jloUhByP\nMEVkoMZ3J7j1ZgI8rAbOkNngX8+pKfTiDz1RC4+dx8oU6Za+4NJXUjlL5CvV6BEYb1+QAEJwitTV\nvxB/A67g42/vzgAtoRUeDov1+GFiBZ+GNF/cAYKcMtGcrs2i97ZkJMo=\n"
104
+ "value": "MIIDQzCCAqygAwIBAgICEAAwDQYJKoZIhvcNAQEFBQAwTjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xMTEwMjIwNjI0MzFaFw0xMjEwMDQwNjI0MzFaMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQKDAtleGFtcGxlLmNvbTEhMB8GA1UEAwwYTXMuIEJhcmJhcmEgSiBKZW5zZW4gSUlJMSIwIAYJKoZIhvcNAQkBFhNiamVuc2VuQGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Kr+Dcds/JQ5GwejJFcBIP682X3xpjis56AK02bc1FLgzdLI8auoR+cC9/Vrh5t66HkQIOdA4unHh0AaZ4xL5PhVbXIPMB5vAPKpzz5iPSi8xO8SL7I7SDhcBVJhqVqr3HgllEG6UClDdHO7nkLuwXq8HcISKkbT5WFTVfFZzidPl8HZ7DhXkZIRtJwBweq4bvm3hM1Os7UQH05ZS6cVDgweKNwdLLrT51ikSQG3DYrl+ft781UQRIqxgwqCfXEuDiinPh0kkvIi5jivVu1Z9QiwlYEdRbLJ4zJQBmDrSGTMYn4lRc2HgHO4DqB/bnMVorHB0CC6AV1QoFK4GPe1LwIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU8pD0U0vsZIsaA16lL8En8bx0F/gwHwYDVR0jBBgwFoAUdGeKitcaF7gnzsNwDx708kqaVt0wDQYJKoZIhvcNAQEFBQADgYEAA81SsFnOdYJtNg5Tcq+/ByEDrBgnusx0jloUhByPMEVkoMZ3J7j1ZgI8rAbOkNngX8+pKfTiDz1RC4+dx8oU6Za+4NJXUjlL5CvV6BEYb1+QAEJwitTVvxB/A67g42/vzgAtoRUeDov1+GFiBZ+GNF/cAYKcMtGcrs2i97ZkJMo="
105
105
  }
106
106
  ],
107
107
  "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
@@ -26,7 +26,9 @@ class Manager(ComplexAttribute):
26
26
 
27
27
 
28
28
  class EnterpriseUser(Extension):
29
- schemas: list[str] = ["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"]
29
+ schemas: Annotated[list[str], Required.true] = [
30
+ "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
31
+ ]
30
32
 
31
33
  employee_number: Optional[str] = None
32
34
  """Numeric or alphanumeric identifier assigned to a person, typically based
@@ -8,6 +8,7 @@ from pydantic import Field
8
8
  from ..base import MultiValuedComplexAttribute
9
9
  from ..base import Mutability
10
10
  from ..base import Reference
11
+ from ..base import Required
11
12
  from .resource import Resource
12
13
 
13
14
 
@@ -31,7 +32,9 @@ class GroupMember(MultiValuedComplexAttribute):
31
32
 
32
33
 
33
34
  class Group(Resource):
34
- schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:Group"]
35
+ schemas: Annotated[list[str], Required.true] = [
36
+ "urn:ietf:params:scim:schemas:core:2.0:Group"
37
+ ]
35
38
 
36
39
  display_name: Optional[str] = None
37
40
  """A human-readable name for the Group."""
@@ -136,7 +136,7 @@ class ResourceMetaclass(BaseModelType):
136
136
 
137
137
 
138
138
  class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
139
- schemas: list[str]
139
+ schemas: Annotated[list[str], Required.true]
140
140
  """The "schemas" attribute is a REQUIRED attribute and is an array of
141
141
  Strings containing URIs that are used to indicate the namespaces of the
142
142
  SCIM schemas that define the attributes present in the current JSON
@@ -229,7 +229,7 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
229
229
  return Resource.get_by_schema(resource_types, schema, **kwargs)
230
230
 
231
231
  @field_serializer("schemas")
232
- def set_extension_schemas(self, schemas: list[str]):
232
+ def set_extension_schemas(self, schemas: Annotated[list[str], Required.true]):
233
233
  """Add model extension ids to the 'schemas' attribute."""
234
234
  extension_schemas = self.get_extension_models().keys()
235
235
  schemas = self.schemas + [
@@ -35,7 +35,9 @@ class SchemaExtension(ComplexAttribute):
35
35
 
36
36
 
37
37
  class ResourceType(Resource):
38
- schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"]
38
+ schemas: Annotated[list[str], Required.true] = [
39
+ "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
40
+ ]
39
41
 
40
42
  name: Annotated[Optional[str], Mutability.read_only, Required.true] = None
41
43
  """The resource type name.
@@ -9,7 +9,6 @@ from typing import Optional
9
9
  from typing import Union
10
10
  from typing import get_origin
11
11
 
12
- from pydantic import Base64Bytes
13
12
  from pydantic import Field
14
13
  from pydantic import create_model
15
14
  from pydantic import field_validator
@@ -30,6 +29,7 @@ from ..base import Uniqueness
30
29
  from ..base import URIReference
31
30
  from ..base import is_complex_attribute
32
31
  from ..constants import RESERVED_WORDS
32
+ from ..utils import Base64Bytes
33
33
  from ..utils import normalize_attribute_name
34
34
  from .resource import Extension
35
35
  from .resource import Resource
@@ -65,7 +65,7 @@ def make_python_model(
65
65
  if attr.name
66
66
  }
67
67
  pydantic_attributes["schemas"] = (
68
- Optional[list[str]],
68
+ Annotated[list[str], Required.true],
69
69
  Field(default=[obj.id]),
70
70
  )
71
71
 
@@ -240,7 +240,9 @@ class Attribute(ComplexAttribute):
240
240
 
241
241
 
242
242
  class Schema(Resource):
243
- schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:Schema"]
243
+ schemas: Annotated[list[str], Required.true] = [
244
+ "urn:ietf:params:scim:schemas:core:2.0:Schema"
245
+ ]
244
246
 
245
247
  id: Annotated[Optional[str], Mutability.read_only, Required.true] = None
246
248
  """The unique URI of the schema."""
@@ -264,3 +266,10 @@ class Schema(Resource):
264
266
  def urn_id(cls, value: str) -> str:
265
267
  """Ensure that schema ids are URI, as defined in RFC7643 §7."""
266
268
  return str(Url(value))
269
+
270
+ def get_attribute(self, attribute_name: str) -> Optional[Attribute]:
271
+ """Find an attribute by its name."""
272
+ for attribute in self.attributes or []:
273
+ if attribute.name == attribute_name:
274
+ return attribute
275
+ return None
@@ -94,7 +94,9 @@ class AuthenticationScheme(ComplexAttribute):
94
94
 
95
95
 
96
96
  class ServiceProviderConfig(Resource):
97
- schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"]
97
+ schemas: Annotated[list[str], Required.true] = [
98
+ "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
99
+ ]
98
100
 
99
101
  id: Annotated[
100
102
  Optional[str], Mutability.read_only, Returned.default, Uniqueness.global_
@@ -4,7 +4,6 @@ from typing import Literal
4
4
  from typing import Optional
5
5
  from typing import Union
6
6
 
7
- from pydantic import Base64Bytes
8
7
  from pydantic import EmailStr
9
8
  from pydantic import Field
10
9
 
@@ -17,6 +16,7 @@ from ..base import Reference
17
16
  from ..base import Required
18
17
  from ..base import Returned
19
18
  from ..base import Uniqueness
19
+ from ..utils import Base64Bytes
20
20
  from .resource import Resource
21
21
 
22
22
 
@@ -214,7 +214,9 @@ class X509Certificate(MultiValuedComplexAttribute):
214
214
 
215
215
 
216
216
  class User(Resource):
217
- schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:User"]
217
+ schemas: Annotated[list[str], Required.true] = [
218
+ "urn:ietf:params:scim:schemas:core:2.0:User"
219
+ ]
218
220
 
219
221
  user_name: Annotated[Optional[str], Uniqueness.server, Required.true] = None
220
222
  """Unique identifier for the User, typically used by the user to directly
@@ -7,6 +7,7 @@ from pydantic import Field
7
7
  from pydantic import PlainSerializer
8
8
 
9
9
  from ..base import ComplexAttribute
10
+ from ..base import Required
10
11
  from ..utils import int_to_str
11
12
  from .message import Message
12
13
 
@@ -53,7 +54,9 @@ class BulkRequest(Message):
53
54
  The models for Bulk operations are defined, but their behavior is not implemented nor tested yet.
54
55
  """
55
56
 
56
- schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:BulkRequest"]
57
+ schemas: Annotated[list[str], Required.true] = [
58
+ "urn:ietf:params:scim:api:messages:2.0:BulkRequest"
59
+ ]
57
60
 
58
61
  fail_on_errors: Optional[int] = None
59
62
  """An integer specifying the number of errors that the service provider
@@ -74,7 +77,9 @@ class BulkResponse(Message):
74
77
  The models for Bulk operations are defined, but their behavior is not implemented nor tested yet.
75
78
  """
76
79
 
77
- schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:BulkResponse"]
80
+ schemas: Annotated[list[str], Required.true] = [
81
+ "urn:ietf:params:scim:api:messages:2.0:BulkResponse"
82
+ ]
78
83
 
79
84
  operations: Optional[list[BulkOperation]] = Field(
80
85
  None, serialization_alias="Operations"
@@ -3,6 +3,7 @@ from typing import Optional
3
3
 
4
4
  from pydantic import PlainSerializer
5
5
 
6
+ from ..base import Required
6
7
  from ..utils import int_to_str
7
8
  from .message import Message
8
9
 
@@ -10,7 +11,9 @@ from .message import Message
10
11
  class Error(Message):
11
12
  """Representation of SCIM API errors."""
12
13
 
13
- schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:Error"]
14
+ schemas: Annotated[list[str], Required.true] = [
15
+ "urn:ietf:params:scim:api:messages:2.0:Error"
16
+ ]
14
17
 
15
18
  status: Annotated[Optional[int], PlainSerializer(int_to_str)] = None
16
19
  """The HTTP status code (see Section 6 of [RFC7231]) expressed as a JSON
@@ -18,6 +18,7 @@ from typing_extensions import Self
18
18
  from ..base import BaseModel
19
19
  from ..base import BaseModelType
20
20
  from ..base import Context
21
+ from ..base import Required
21
22
  from ..rfc7643.resource import AnyResource
22
23
  from .message import Message
23
24
 
@@ -78,7 +79,9 @@ class ListResponseMetaclass(BaseModelType):
78
79
 
79
80
 
80
81
  class ListResponse(Message, Generic[AnyResource], metaclass=ListResponseMetaclass):
81
- schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:ListResponse"]
82
+ schemas: Annotated[list[str], Required.true] = [
83
+ "urn:ietf:params:scim:api:messages:2.0:ListResponse"
84
+ ]
82
85
 
83
86
  total_results: Optional[int] = None
84
87
  """The total number of results returned by the list or query operation."""
@@ -1,7 +1,10 @@
1
+ from typing import Annotated
2
+
1
3
  from ..base import BaseModel
4
+ from ..base import Required
2
5
 
3
6
 
4
7
  class Message(BaseModel):
5
8
  """SCIM protocol messages as defined by :rfc:`RFC7644 §3.1 <7644#section-3.1>`."""
6
9
 
7
- schemas: list[str]
10
+ schemas: Annotated[list[str], Required.true]
@@ -1,4 +1,5 @@
1
1
  from enum import Enum
2
+ from typing import Annotated
2
3
  from typing import Any
3
4
  from typing import Optional
4
5
 
@@ -6,6 +7,7 @@ from pydantic import Field
6
7
  from pydantic import field_validator
7
8
 
8
9
  from ..base import ComplexAttribute
10
+ from ..base import Required
9
11
  from .message import Message
10
12
 
11
13
 
@@ -57,7 +59,9 @@ class PatchOp(Message):
57
59
  The models for Patch operations are defined, but their behavior is not implemented nor tested yet.
58
60
  """
59
61
 
60
- schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]
62
+ schemas: Annotated[list[str], Required.true] = [
63
+ "urn:ietf:params:scim:api:messages:2.0:PatchOp"
64
+ ]
61
65
 
62
66
  operations: Optional[list[PatchOperation]] = Field(
63
67
  None, serialization_alias="Operations"
@@ -1,9 +1,11 @@
1
1
  from enum import Enum
2
+ from typing import Annotated
2
3
  from typing import Optional
3
4
 
4
5
  from pydantic import field_validator
5
6
  from pydantic import model_validator
6
7
 
8
+ from ..base import Required
7
9
  from .message import Message
8
10
 
9
11
 
@@ -13,7 +15,9 @@ class SearchRequest(Message):
13
15
  https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.3
14
16
  """
15
17
 
16
- schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:SearchRequest"]
18
+ schemas: Annotated[list[str], Required.true] = [
19
+ "urn:ietf:params:scim:api:messages:2.0:SearchRequest"
20
+ ]
17
21
 
18
22
  attributes: Optional[list[str]] = None
19
23
  """A multi-valued list of strings indicating the names of resource
@@ -72,3 +76,17 @@ class SearchRequest(Message):
72
76
  )
73
77
 
74
78
  return self
79
+
80
+ @property
81
+ def start_index_0(self):
82
+ """The 0 indexed start index."""
83
+ return self.start_index - 1 if self.start_index is not None else None
84
+
85
+ @property
86
+ def stop_index_0(self):
87
+ """The 0 indexed stop index."""
88
+ return (
89
+ self.start_index_0 + self.count
90
+ if self.start_index_0 is not None and self.count is not None
91
+ else None
92
+ )
@@ -0,0 +1,99 @@
1
+ import base64
2
+ import re
3
+ from typing import Annotated
4
+ from typing import Literal
5
+ from typing import Optional
6
+ from typing import Union
7
+
8
+ from pydantic import EncodedBytes
9
+ from pydantic import EncoderProtocol
10
+ from pydantic.alias_generators import to_snake
11
+ from pydantic_core import PydanticCustomError
12
+
13
+ try:
14
+ from types import UnionType # type: ignore
15
+
16
+ UNION_TYPES = [Union, UnionType]
17
+ except ImportError:
18
+ # Python 3.9 has no UnionType
19
+ UNION_TYPES = [Union]
20
+
21
+
22
+ def int_to_str(status: Optional[int]) -> Optional[str]:
23
+ return None if status is None else str(status)
24
+
25
+
26
+ # Copied from Pydantic 2.10 repository
27
+ class Base64Encoder(EncoderProtocol): # pragma: no cover
28
+ """Standard (non-URL-safe) Base64 encoder."""
29
+
30
+ @classmethod
31
+ def decode(cls, data: bytes) -> bytes:
32
+ """Decode the data from base64 encoded bytes to original bytes data.
33
+
34
+ Args:
35
+ data: The data to decode.
36
+
37
+ Returns:
38
+ The decoded data.
39
+
40
+ """
41
+ try:
42
+ return base64.b64decode(data)
43
+ except ValueError as e:
44
+ raise PydanticCustomError(
45
+ "base64_decode", "Base64 decoding error: '{error}'", {"error": str(e)}
46
+ ) from e
47
+
48
+ @classmethod
49
+ def encode(cls, value: bytes) -> bytes:
50
+ """Encode the data from bytes to a base64 encoded bytes.
51
+
52
+ Args:
53
+ value: The data to encode.
54
+
55
+ Returns:
56
+ The encoded data.
57
+
58
+ """
59
+ return base64.b64encode(value)
60
+
61
+ @classmethod
62
+ def get_json_format(cls) -> Literal["base64"]:
63
+ """Get the JSON format for the encoded data.
64
+
65
+ Returns:
66
+ The JSON format for the encoded data.
67
+
68
+ """
69
+ return "base64"
70
+
71
+
72
+ # Compatibility with Pydantic <2.10
73
+ # https://pydantic.dev/articles/pydantic-v2-10-release#use-b64decode-and-b64encode-for-base64bytes-and-base64str-types
74
+ Base64Bytes = Annotated[bytes, EncodedBytes(encoder=Base64Encoder)]
75
+
76
+
77
+ def to_camel(string: str) -> str:
78
+ """Transform strings to camelCase.
79
+
80
+ This method is used for attribute name serialization. This is more
81
+ or less the pydantic implementation, but it does not add uppercase
82
+ on alphanumerical characters after specials characters. For instance
83
+ '$ref' stays '$ref'.
84
+ """
85
+ snake = to_snake(string)
86
+ camel = re.sub(r"_+([0-9A-Za-z]+)", lambda m: m.group(1).title(), snake)
87
+ return camel
88
+
89
+
90
+ def normalize_attribute_name(attribute_name: str) -> str:
91
+ """Remove all non-alphabetical characters and lowerise a string.
92
+
93
+ This method is used for attribute name validation.
94
+ """
95
+ is_extension_attribute = ":" in attribute_name
96
+ if not is_extension_attribute:
97
+ attribute_name = re.sub(r"[\W_]+", "", attribute_name)
98
+
99
+ return attribute_name.lower()
@@ -2,8 +2,6 @@ import datetime
2
2
  from typing import Literal
3
3
  from typing import Union
4
4
 
5
- from pydantic import Base64Bytes
6
-
7
5
  from scim2_models.base import CaseExact
8
6
  from scim2_models.base import ComplexAttribute
9
7
  from scim2_models.base import ExternalReference
@@ -18,6 +16,7 @@ from scim2_models.rfc7643.resource import Extension
18
16
  from scim2_models.rfc7643.resource import Resource
19
17
  from scim2_models.rfc7643.schema import Attribute
20
18
  from scim2_models.rfc7643.schema import Schema
19
+ from scim2_models.utils import Base64Bytes
21
20
 
22
21
 
23
22
  def test_make_group_model_from_schema(load_sample):
@@ -1,3 +1,4 @@
1
+ from typing import Annotated
1
2
  from typing import Union
2
3
 
3
4
  import pytest
@@ -7,6 +8,7 @@ from scim2_models import Context
7
8
  from scim2_models import EnterpriseUser
8
9
  from scim2_models import Group
9
10
  from scim2_models import ListResponse
11
+ from scim2_models import Required
10
12
  from scim2_models import Resource
11
13
  from scim2_models import ResourceType
12
14
  from scim2_models import ServiceProviderConfig
@@ -107,7 +109,7 @@ def test_mixed_types(load_sample):
107
109
 
108
110
 
109
111
  class Foobar(Resource):
110
- schemas: list[str] = ["foobarschema"]
112
+ schemas: Annotated[list[str], Required.true] = ["foobarschema"]
111
113
 
112
114
 
113
115
  def test_mixed_types_type_missing(load_sample):
@@ -7,9 +7,11 @@ import pytest
7
7
  from scim2_models.base import BaseModel
8
8
  from scim2_models.base import ComplexAttribute
9
9
  from scim2_models.base import Context
10
+ from scim2_models.base import Required
10
11
  from scim2_models.base import Returned
11
12
  from scim2_models.base import validate_attribute_urn
12
13
  from scim2_models.rfc7643.enterprise_user import EnterpriseUser
14
+ from scim2_models.rfc7643.resource import Extension
13
15
  from scim2_models.rfc7643.resource import Meta
14
16
  from scim2_models.rfc7643.resource import Resource
15
17
  from scim2_models.rfc7643.user import User
@@ -20,7 +22,7 @@ class Sub(ComplexAttribute):
20
22
 
21
23
 
22
24
  class Sup(Resource):
23
- schemas: list[str] = ["urn:example:2.0:Sup"]
25
+ schemas: Annotated[list[str], Required.true] = ["urn:example:2.0:Sup"]
24
26
  dummy: str
25
27
  sub: Sub
26
28
  subs: list[Sub]
@@ -44,7 +46,7 @@ class Baz(ComplexAttribute):
44
46
 
45
47
 
46
48
  class Foo(Resource):
47
- schemas: list[str] = ["urn:example:2.0:Foo"]
49
+ schemas: Annotated[list[str], Required.true] = ["urn:example:2.0:Foo"]
48
50
  sub: Annotated[ReturnedModel, Returned.default]
49
51
  bar: str
50
52
  snake_case: str
@@ -52,15 +54,15 @@ class Foo(Resource):
52
54
 
53
55
 
54
56
  class Bar(Resource):
55
- schemas: list[str] = ["urn:example:2.0:Bar"]
57
+ schemas: Annotated[list[str], Required.true] = ["urn:example:2.0:Bar"]
56
58
  sub: Annotated[ReturnedModel, Returned.default]
57
59
  bar: str
58
60
  snake_case: str
59
61
  baz: Optional[Baz] = None
60
62
 
61
63
 
62
- class Extension(Resource):
63
- schemas: list[str] = ["urn:example:2.0:Extension"]
64
+ class MyExtension(Extension):
65
+ schemas: Annotated[list[str], Required.true] = ["urn:example:2.0:MyExtension"]
64
66
  baz: str
65
67
 
66
68
 
@@ -105,14 +107,14 @@ def test_validate_attribute_urn():
105
107
  )
106
108
 
107
109
  assert (
108
- validate_attribute_urn("urn:example:2.0:Extension:baz", Foo[Extension])
109
- == "urn:example:2.0:Extension:baz"
110
+ validate_attribute_urn("urn:example:2.0:MyExtension:baz", Foo[MyExtension])
111
+ == "urn:example:2.0:MyExtension:baz"
110
112
  )
111
113
  assert (
112
114
  validate_attribute_urn(
113
- "urn:example:2.0:Extension:baz", resource_types=[Foo[Extension]]
115
+ "urn:example:2.0:MyExtension:baz", resource_types=[Foo[MyExtension]]
114
116
  )
115
- == "urn:example:2.0:Extension:baz"
117
+ == "urn:example:2.0:MyExtension:baz"
116
118
  )
117
119
 
118
120
  with pytest.raises(ValueError, match="No default schema and relative URN"):
@@ -299,7 +301,7 @@ def test_dump_after_assignment():
299
301
 
300
302
  def test_binary_attributes():
301
303
  decoded = b"This is a very long line with a lot of characters, enough to create newlines when encoded."
302
- encoded = "VGhpcyBpcyBhIHZlcnkgbG9uZyBsaW5lIHdpdGggYSBsb3Qgb2YgY2hhcmFjdGVycywgZW5vdWdo\nIHRvIGNyZWF0ZSBuZXdsaW5lcyB3aGVuIGVuY29kZWQu\n"
304
+ encoded = "VGhpcyBpcyBhIHZlcnkgbG9uZyBsaW5lIHdpdGggYSBsb3Qgb2YgY2hhcmFjdGVycywgZW5vdWdoIHRvIGNyZWF0ZSBuZXdsaW5lcyB3aGVuIGVuY29kZWQu"
303
305
 
304
306
  user = User.model_validate(
305
307
  {"userName": "foobar", "x509Certificates": [{"value": encoded}]}
@@ -317,7 +319,7 @@ def test_binary_attributes():
317
319
  assert user.x509_certificates[0].value == decoded
318
320
  assert user.model_dump()["x509Certificates"][0]["value"] == encoded
319
321
 
320
- encoded_with_padding = "VGhpcyBpcyBhIHZlcnkgbG9uZyBsaW5lIHdpdGggYSBsb3Qgb2YgY2hhcmFjdGVycywgZW5vdWdo\nIHRvIGNyZWF0ZSBuZXdsaW5lcyB3aGVuIGVuY29kZWQu==================\n"
322
+ encoded_with_padding = "VGhpcyBpcyBhIHZlcnkgbG9uZyBsaW5lIHdpdGggYSBsb3Qgb2YgY2hhcmFjdGVycywgZW5vdWdoIHRvIGNyZWF0ZSBuZXdsaW5lcyB3aGVuIGVuY29kZWQu=================="
321
323
  user = User.model_validate(
322
324
  {"userName": "foobar", "x509Certificates": [{"value": encoded_with_padding}]}
323
325
  )
@@ -6,6 +6,7 @@ import pytest
6
6
  from scim2_models.base import ComplexAttribute
7
7
  from scim2_models.base import Context
8
8
  from scim2_models.base import Mutability
9
+ from scim2_models.base import Required
9
10
  from scim2_models.base import Returned
10
11
  from scim2_models.rfc7643.resource import Resource
11
12
 
@@ -18,7 +19,7 @@ class SubRetModel(ComplexAttribute):
18
19
 
19
20
 
20
21
  class SupRetResource(Resource):
21
- schemas: list[str] = ["org:example:SupRetResource"]
22
+ schemas: Annotated[list[str], Required.true] = ["org:example:SupRetResource"]
22
23
 
23
24
  always_returned: Annotated[Optional[str], Returned.always] = None
24
25
  never_returned: Annotated[Optional[str], Returned.never] = None
@@ -29,7 +30,7 @@ class SupRetResource(Resource):
29
30
 
30
31
 
31
32
  class MutResource(Resource):
32
- schemas: list[str] = ["org:example:MutResource"]
33
+ schemas: Annotated[list[str], Required.true] = ["org:example:MutResource"]
33
34
 
34
35
  read_only: Annotated[Optional[str], Mutability.read_only] = None
35
36
  read_write: Annotated[Optional[str], Mutability.read_write] = None
@@ -12,7 +12,7 @@ from scim2_models.rfc7643.resource import Resource
12
12
 
13
13
 
14
14
  class RetResource(Resource):
15
- schemas: list[str] = ["org:example:RetResource"]
15
+ schemas: Annotated[list[str], Required.true] = ["org:example:RetResource"]
16
16
 
17
17
  always_returned: Annotated[Optional[str], Returned.always] = None
18
18
  never_returned: Annotated[Optional[str], Returned.never] = None
@@ -21,7 +21,7 @@ class RetResource(Resource):
21
21
 
22
22
 
23
23
  class MutResource(Resource):
24
- schemas: list[str] = ["org:example:MutResource"]
24
+ schemas: Annotated[list[str], Required.true] = ["org:example:MutResource"]
25
25
 
26
26
  read_only: Annotated[Optional[str], Mutability.read_only] = None
27
27
  read_write: Annotated[Optional[str], Mutability.read_write] = None
@@ -30,7 +30,7 @@ class MutResource(Resource):
30
30
 
31
31
 
32
32
  class ReqResource(Resource):
33
- schemas: list[str] = ["org:example:ReqResource"]
33
+ schemas: Annotated[list[str], Required.true] = ["org:example:ReqResource"]
34
34
 
35
35
  required: Annotated[Optional[str], Required.true] = None
36
36
  optional: Annotated[Optional[str], Required.false] = None