scim2-cli 0.2.4__tar.gz → 0.4.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scim2-cli
3
- Version: 0.2.4
3
+ Version: 0.4.0
4
4
  Summary: SCIM application development CLI
5
5
  Keywords: scim,scim2,provisioning,rfc7643,rfc7644
6
6
  Author: Yaal Coop
@@ -208,7 +208,6 @@ License: Apache License
208
208
  limitations under the License.
209
209
  Classifier: Intended Audience :: Developers
210
210
  Classifier: Development Status :: 3 - Alpha
211
- Classifier: Programming Language :: Python :: 3.10
212
211
  Classifier: Programming Language :: Python :: 3.11
213
212
  Classifier: Programming Language :: Python :: 3.12
214
213
  Classifier: Programming Language :: Python :: 3.13
@@ -219,16 +218,16 @@ Classifier: Environment :: Web Environment
219
218
  Classifier: Programming Language :: Python
220
219
  Classifier: Operating System :: OS Independent
221
220
  Requires-Dist: click>=8.1.7
222
- Requires-Dist: scim2-client>=0.7.1
223
- Requires-Dist: scim2-tester[httpx]>=0.2.5
221
+ Requires-Dist: scim2-client[httpx2]>=0.9.0
222
+ Requires-Dist: scim2-tester>=0.4.0
224
223
  Requires-Dist: sphinx-click-rst-to-ansi-formatter>=0.1.0
225
- Requires-Dist: pydanclick>=0.4.0
224
+ Requires-Dist: pydanclick>=0.5.0
226
225
  Requires-Dist: pygments>=2.18.0
227
- Requires-Python: >=3.10
228
- Project-URL: changelog, https://scim2-cli.readthedocs.io/en/latest/changelog.html
226
+ Requires-Python: >=3.11
229
227
  Project-URL: documentation, https://scim2-cli.readthedocs.io
230
- Project-URL: funding, https://github.com/sponsors/python-scim
231
228
  Project-URL: repository, https://github.com/python-scim/scim2-cli
229
+ Project-URL: changelog, https://scim2-cli.readthedocs.io/en/latest/changelog.html
230
+ Project-URL: funding, https://github.com/sponsors/python-scim
232
231
  Description-Content-Type: text/markdown
233
232
 
234
233
  # scim2-cli
@@ -242,44 +241,24 @@ Provisioning is the action of managing a set of resources across different servi
242
241
  SCIM is often used between Identity Providers and applications in completion of standards like OAuth2 and OpenID Connect.
243
242
  It allows users and groups creations, modifications and deletions to be synchronized between applications.
244
243
 
245
- ## Installation
244
+ ## Features
246
245
 
247
- ### Binary files
246
+ - **CRUD Commands**: `create`, `query`, `replace` and `delete` resources from the command line
247
+ - **Search Command**: Query resources using SCIM filters, sorting and pagination
248
+ - **Compliance Testing**: Built-in `test` command using [scim2-tester](https://scim2-tester.readthedocs.io) to validate server RFC compliance
249
+ - **Server Discovery**: Automatic retrieval of server schemas and resource types
250
+ - **Dynamic CLI Options**: Command options generated from server schemas
251
+ - **Flexible Input**: Pass attributes as CLI options or pipe JSON payloads via stdin
252
+ - **JSON Output**: Formatted JSON responses with optional indentation control
253
+ - **Authentication**: Custom HTTP headers support for Bearer tokens and other auth methods
248
254
 
249
- Binary files are available on the [releases page](https://github.com/python-scim/scim2-cli/releases).
250
-
251
- ### Python package
252
-
253
- scim2-cli is published on the [PyPI repository](https://pypi.org/project/scim2-cli).
254
- You can install it with your favourite python package manager.
255
-
256
- ```shell
257
- pip install scim2-cli
258
- ```
259
-
260
- ### From sources
261
-
262
- To run scim2-cli from the sources, [uv is needed](https://docs.astral.sh/uv/getting-started/installation/) for development:
263
-
264
- ```shell
265
- git clone https://github.com/python-scim/scim2-cli.git
266
- cd scim2-cli
267
- uv sync
268
- ```
269
-
270
- Then, it can be launched directly as a Python script:
255
+ ## Installation
271
256
 
272
- ```shell
273
- uv run scim2 --help
257
+ ```console
258
+ $ curl -LsSf uvx.sh/scim2-cli/install.sh | sh
274
259
  ```
275
260
 
276
- Or you can build a single file binary from the sources:
277
-
278
- ```shell
279
- uv sync --group bundle
280
- uv run pyinstaller --name scim2 --onefile scim2_cli/__init__.py
281
- ./dist/scim2 --help
282
- ```
261
+ Check the [installation documentation](https://scim2-cli.readthedocs.io/en/latest/installation.html) for other methods.
283
262
 
284
263
  ## Usage
285
264
 
@@ -287,7 +266,7 @@ Check the [reference](https://scim2-cli.readthedocs.io/en/latest/reference.html)
287
266
 
288
267
  Here is an example of resource creation:
289
268
 
290
- ```shell
269
+ ```console
291
270
  $ scim2 --url https://auth.example --header "Authorization: Bearer 12345" create user --user-name "bjensen@example.com"
292
271
  {
293
272
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -305,7 +284,7 @@ $ scim2 --url https://auth.example --header "Authorization: Bearer 12345" create
305
284
 
306
285
  Here is an example of resource query:
307
286
 
308
- ```shell
287
+ ```console
309
288
  $ scim2 --url https://auth.example --header "Authorization: Bearer 12345" query user 2819c223-7f76-453a-919d-413861904646
310
289
  {
311
290
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -9,44 +9,24 @@ Provisioning is the action of managing a set of resources across different servi
9
9
  SCIM is often used between Identity Providers and applications in completion of standards like OAuth2 and OpenID Connect.
10
10
  It allows users and groups creations, modifications and deletions to be synchronized between applications.
11
11
 
12
- ## Installation
13
-
14
- ### Binary files
15
-
16
- Binary files are available on the [releases page](https://github.com/python-scim/scim2-cli/releases).
17
-
18
- ### Python package
19
-
20
- scim2-cli is published on the [PyPI repository](https://pypi.org/project/scim2-cli).
21
- You can install it with your favourite python package manager.
22
-
23
- ```shell
24
- pip install scim2-cli
25
- ```
12
+ ## Features
26
13
 
27
- ### From sources
14
+ - **CRUD Commands**: `create`, `query`, `replace` and `delete` resources from the command line
15
+ - **Search Command**: Query resources using SCIM filters, sorting and pagination
16
+ - **Compliance Testing**: Built-in `test` command using [scim2-tester](https://scim2-tester.readthedocs.io) to validate server RFC compliance
17
+ - **Server Discovery**: Automatic retrieval of server schemas and resource types
18
+ - **Dynamic CLI Options**: Command options generated from server schemas
19
+ - **Flexible Input**: Pass attributes as CLI options or pipe JSON payloads via stdin
20
+ - **JSON Output**: Formatted JSON responses with optional indentation control
21
+ - **Authentication**: Custom HTTP headers support for Bearer tokens and other auth methods
28
22
 
29
- To run scim2-cli from the sources, [uv is needed](https://docs.astral.sh/uv/getting-started/installation/) for development:
30
-
31
- ```shell
32
- git clone https://github.com/python-scim/scim2-cli.git
33
- cd scim2-cli
34
- uv sync
35
- ```
36
-
37
- Then, it can be launched directly as a Python script:
23
+ ## Installation
38
24
 
39
- ```shell
40
- uv run scim2 --help
25
+ ```console
26
+ $ curl -LsSf uvx.sh/scim2-cli/install.sh | sh
41
27
  ```
42
28
 
43
- Or you can build a single file binary from the sources:
44
-
45
- ```shell
46
- uv sync --group bundle
47
- uv run pyinstaller --name scim2 --onefile scim2_cli/__init__.py
48
- ./dist/scim2 --help
49
- ```
29
+ Check the [installation documentation](https://scim2-cli.readthedocs.io/en/latest/installation.html) for other methods.
50
30
 
51
31
  ## Usage
52
32
 
@@ -54,7 +34,7 @@ Check the [reference](https://scim2-cli.readthedocs.io/en/latest/reference.html)
54
34
 
55
35
  Here is an example of resource creation:
56
36
 
57
- ```shell
37
+ ```console
58
38
  $ scim2 --url https://auth.example --header "Authorization: Bearer 12345" create user --user-name "bjensen@example.com"
59
39
  {
60
40
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -72,7 +52,7 @@ $ scim2 --url https://auth.example --header "Authorization: Bearer 12345" create
72
52
 
73
53
  Here is an example of resource query:
74
54
 
75
- ```shell
55
+ ```console
76
56
  $ scim2 --url https://auth.example --header "Authorization: Bearer 12345" query user 2819c223-7f76-453a-919d-413861904646
77
57
  {
78
58
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -0,0 +1,239 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.12.13,<0.13.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "scim2-cli"
7
+ version = "0.4.0"
8
+ description = "SCIM application development CLI"
9
+ readme = "README.md"
10
+ keywords = [
11
+ "scim",
12
+ "scim2",
13
+ "provisioning",
14
+ "rfc7643",
15
+ "rfc7644",
16
+ ]
17
+ classifiers = [
18
+ "Intended Audience :: Developers",
19
+ "Development Status :: 3 - Alpha",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Programming Language :: Python :: Implementation :: CPython",
25
+ "License :: OSI Approved :: Apache Software License",
26
+ "Environment :: Web Environment",
27
+ "Programming Language :: Python",
28
+ "Operating System :: OS Independent",
29
+ ]
30
+ requires-python = ">= 3.11"
31
+ dependencies = [
32
+ "click>=8.1.7",
33
+ "scim2-client[httpx2]>=0.9.0",
34
+ "scim2-tester>=0.4.0",
35
+ "sphinx-click-rst-to-ansi-formatter>=0.1.0",
36
+ "pydanclick>=0.5.0",
37
+ "pygments>=2.18.0",
38
+ ]
39
+
40
+ [[project.authors]]
41
+ name = "Yaal Coop"
42
+ email = "contact@yaal.coop"
43
+
44
+ [project.license]
45
+ file = "LICENSE.md"
46
+
47
+ [project.urls]
48
+ documentation = "https://scim2-cli.readthedocs.io"
49
+ repository = "https://github.com/python-scim/scim2-cli"
50
+ changelog = "https://scim2-cli.readthedocs.io/en/latest/changelog.html"
51
+ funding = "https://github.com/sponsors/python-scim"
52
+
53
+ [project.scripts]
54
+ scim = "scim2_cli:cli"
55
+ scim2 = "scim2_cli:cli"
56
+
57
+ [dependency-groups]
58
+ dev = [
59
+ "mypy>=1.13.0",
60
+ "prek>=0.1.0",
61
+ "pytest>=8.2.1",
62
+ "pytest-cov>=6.0.0",
63
+ "pytest-httpserver>=1.0.10",
64
+ "tox-uv>=1.16.0",
65
+ ]
66
+ doc = [
67
+ "myst-parser>=3.0.1",
68
+ "shibuya>=2024.5.15",
69
+ "sphinx>=7.3.7",
70
+ "sphinx-click>=6.0.0",
71
+ "sphinx-design>=0.6.0",
72
+ "sphinx-iconify>=0.0.6",
73
+ "sphinx-issues>=5.0.0",
74
+ "sphinx-substitution-extensions>=2024.8.6",
75
+ ]
76
+ bundle = ["pyinstaller>=6.10.0"]
77
+
78
+ [tool.uv]
79
+ exclude-newer = "14 days"
80
+
81
+ [tool.uv.exclude-newer-package]
82
+ scim2-models = false
83
+ scim2-client = false
84
+ scim2-server = false
85
+ scim2-cli = false
86
+ scim2-tester = false
87
+ pytest-scim2-server = false
88
+
89
+ [tool.uv.build-backend]
90
+ module-root = ""
91
+
92
+ [tool.coverage.run]
93
+ source = [
94
+ "scim2_cli",
95
+ "tests",
96
+ ]
97
+ omit = [".tox/*"]
98
+ branch = true
99
+
100
+ [tool.coverage.report]
101
+ exclude_lines = [
102
+ "@pytest.mark.skip",
103
+ "pragma: no cover",
104
+ "raise NotImplementedError",
105
+ "except ImportError",
106
+ "if app.debug",
107
+ ]
108
+
109
+ [tool.ruff.lint]
110
+ select = [
111
+ "B",
112
+ "D",
113
+ "E",
114
+ "F",
115
+ "I",
116
+ "UP",
117
+ ]
118
+ ignore = [
119
+ "E501",
120
+ "E722",
121
+ "D100",
122
+ "D101",
123
+ "D102",
124
+ "D103",
125
+ "D104",
126
+ "D105",
127
+ "D106",
128
+ "D107",
129
+ "D203",
130
+ "D213",
131
+ ]
132
+
133
+ [tool.ruff.lint.isort]
134
+ force-single-line = true
135
+
136
+ [tool.ruff.format]
137
+ docstring-code-format = true
138
+
139
+ [tool.codespell]
140
+ ignore-words-list = "datas"
141
+
142
+ [tool.mypy]
143
+ plugins = ["pydantic.mypy"]
144
+
145
+ [tool.pytest.ini_options]
146
+ testpaths = ["tests"]
147
+
148
+ [tool.tox]
149
+ requires = ["tox>=4.19"]
150
+ env_list = [
151
+ "style",
152
+ "py311",
153
+ "py312",
154
+ "py313",
155
+ "py314",
156
+ "minversions",
157
+ "doc",
158
+ "bundle",
159
+ "coverage",
160
+ ]
161
+
162
+ [tool.tox.env_run_base]
163
+ runner = "uv-venv-lock-runner"
164
+ dependency_groups = ["dev"]
165
+ commands = [[
166
+ "pytest",
167
+ "--showlocals",
168
+ "--full-trace",
169
+ "{posargs}",
170
+ ]]
171
+
172
+ [tool.tox.env.style]
173
+ runner = "uv-venv-runner"
174
+ commands = [[
175
+ "prek",
176
+ "run",
177
+ "--all-files",
178
+ "--show-diff-on-failure",
179
+ ]]
180
+
181
+ [tool.tox.env.minversions]
182
+ runner = "uv-venv-runner"
183
+ uv_resolution = "lowest-direct"
184
+
185
+ [tool.tox.env.doc]
186
+ dependency_groups = ["doc"]
187
+ commands = [
188
+ [
189
+ "sphinx-build",
190
+ "-W",
191
+ "--keep-going",
192
+ "--builder",
193
+ "html",
194
+ "doc",
195
+ "build/sphinx/html",
196
+ ],
197
+ [
198
+ "sphinx-build",
199
+ "-W",
200
+ "--keep-going",
201
+ "--builder",
202
+ "man",
203
+ "doc",
204
+ "build/sphinx/html",
205
+ ],
206
+ ]
207
+
208
+ [tool.tox.env.coverage]
209
+ commands = [
210
+ [
211
+ "pytest",
212
+ "--cov",
213
+ "--cov-fail-under=100",
214
+ "--cov-report",
215
+ "term:skip-covered",
216
+ "{posargs}",
217
+ ],
218
+ [
219
+ "coverage",
220
+ "html",
221
+ ],
222
+ ]
223
+
224
+ [tool.tox.env.bundle]
225
+ dependency_groups = ["bundle"]
226
+ allowlist_externals = ["./dist/scim2"]
227
+ commands = [
228
+ [
229
+ "pyinstaller",
230
+ "--clean",
231
+ "--log-level",
232
+ "WARN",
233
+ "scim2.spec",
234
+ ],
235
+ [
236
+ "./dist/scim2",
237
+ "--help",
238
+ ],
239
+ ]
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.8.9,<0.9.0"]
2
+ requires = ["uv_build>=0.12.13,<0.13.0"]
3
3
  build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-cli"
7
- version = "0.2.4"
7
+ version = "0.4.0"
8
8
  description = "SCIM application development CLI"
9
9
  authors = [{name="Yaal Coop", email="contact@yaal.coop"}]
10
10
  license = {file = "LICENSE.md"}
@@ -13,7 +13,6 @@ keywords = ["scim", "scim2", "provisioning", "rfc7643", "rfc7644"]
13
13
  classifiers = [
14
14
  "Intended Audience :: Developers",
15
15
  "Development Status :: 3 - Alpha",
16
- "Programming Language :: Python :: 3.10",
17
16
  "Programming Language :: Python :: 3.11",
18
17
  "Programming Language :: Python :: 3.12",
19
18
  "Programming Language :: Python :: 3.13",
@@ -25,13 +24,13 @@ classifiers = [
25
24
  "Operating System :: OS Independent",
26
25
  ]
27
26
 
28
- requires-python = ">= 3.10"
27
+ requires-python = ">= 3.11"
29
28
  dependencies = [
30
29
  "click>=8.1.7",
31
- "scim2-client>=0.7.1",
32
- "scim2-tester[httpx]>=0.2.5",
30
+ "scim2-client[httpx2]>=0.9.0",
31
+ "scim2-tester>=0.4.0",
33
32
  "sphinx-click-rst-to-ansi-formatter>=0.1.0",
34
- "pydanclick>=0.4.0",
33
+ "pydanclick>=0.5.0",
35
34
  "pygments>=2.18.0",
36
35
  ]
37
36
 
@@ -46,16 +45,19 @@ dev = [
46
45
  "mypy>=1.13.0",
47
46
  "prek>=0.1.0",
48
47
  "pytest>=8.2.1",
49
- "pytest-coverage>=0.0",
48
+ "pytest-cov>=6.0.0",
50
49
  "pytest-httpserver>=1.0.10",
51
50
  "tox-uv>=1.16.0",
52
51
  ]
53
52
  doc = [
53
+ "myst-parser>=3.0.1",
54
54
  "shibuya>=2024.5.15",
55
55
  "sphinx>=7.3.7",
56
56
  "sphinx-click>=6.0.0",
57
- "sphinx-issues >= 5.0.0",
58
- "myst-parser>=3.0.1",
57
+ "sphinx-design>=0.6.0",
58
+ "sphinx-iconify>=0.0.6",
59
+ "sphinx-issues>=5.0.0",
60
+ "sphinx-substitution-extensions>=2024.8.6",
59
61
  ]
60
62
  bundle = [
61
63
  "pyinstaller>=6.10.0",
@@ -65,6 +67,10 @@ bundle = [
65
67
  scim = "scim2_cli:cli"
66
68
  scim2 = "scim2_cli:cli"
67
69
 
70
+ [tool.uv]
71
+ exclude-newer = "14 days"
72
+ exclude-newer-package = { scim2-models = false, scim2-client = false, scim2-server = false, scim2-cli = false, scim2-tester = false, pytest-scim2-server = false }
73
+
68
74
  [tool.uv.build-backend]
69
75
  module-root = ""
70
76
 
@@ -115,22 +121,28 @@ force-single-line = true
115
121
  [tool.ruff.format]
116
122
  docstring-code-format = true
117
123
 
124
+ [tool.codespell]
125
+ ignore-words-list = "datas"
126
+
118
127
  [tool.mypy]
119
128
  plugins = [
120
129
  "pydantic.mypy"
121
130
  ]
122
131
 
132
+ [tool.pytest.ini_options]
133
+ testpaths = ["tests"]
134
+
123
135
  [tool.tox]
124
136
  requires = ["tox>=4.19"]
125
137
  env_list = [
126
138
  "style",
127
- "py310",
128
139
  "py311",
129
140
  "py312",
130
141
  "py313",
131
142
  "py314",
132
143
  "minversions",
133
144
  "doc",
145
+ "bundle",
134
146
  "coverage",
135
147
  ]
136
148
 
@@ -154,8 +166,8 @@ uv_resolution = "lowest-direct"
154
166
  [tool.tox.env.doc]
155
167
  dependency_groups = ["doc"]
156
168
  commands = [
157
- ["sphinx-build", "--builder", "html", "doc", "build/sphinx/html"],
158
- ["sphinx-build", "--builder", "man", "doc", "build/sphinx/html"],
169
+ ["sphinx-build", "-W", "--keep-going", "--builder", "html", "doc", "build/sphinx/html"],
170
+ ["sphinx-build", "-W", "--keep-going", "--builder", "man", "doc", "build/sphinx/html"],
159
171
  ]
160
172
 
161
173
  [tool.tox.env.coverage]
@@ -163,3 +175,11 @@ commands = [
163
175
  ["pytest", "--cov", "--cov-fail-under=100", "--cov-report", "term:skip-covered", "{posargs}"],
164
176
  ["coverage", "html"],
165
177
  ]
178
+
179
+ [tool.tox.env.bundle]
180
+ dependency_groups = ["bundle"]
181
+ allowlist_externals = ["./dist/scim2"]
182
+ commands = [
183
+ ["pyinstaller", "--clean", "--log-level", "WARN", "scim2.spec"],
184
+ ["./dist/scim2", "--help"],
185
+ ]
@@ -2,16 +2,14 @@ import json
2
2
  import re
3
3
 
4
4
  import click
5
- from httpx import Client
6
- from scim2_client import SCIMClientError
7
- from scim2_client.engines.httpx import SyncSCIMClient
8
- from scim2_models import Group
5
+ from httpx2 import Client
6
+ from scim2_client.engines.httpx2 import SyncSCIMClient
9
7
  from scim2_models import ListResponse
10
- from scim2_models import Resource
11
8
  from scim2_models import ResourceType
12
9
  from scim2_models import Schema
10
+ from scim2_models import ScimProvider
11
+ from scim2_models import ScimProviderError
13
12
  from scim2_models import ServiceProviderConfig
14
- from scim2_models import User
15
13
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
16
14
 
17
15
  from scim2_cli.create import create_cli
@@ -21,49 +19,41 @@ from scim2_cli.replace import replace_cli
21
19
  from scim2_cli.search import search_cli
22
20
  from scim2_cli.test import test_cli
23
21
  from scim2_cli.utils import DOC_URL
22
+ from scim2_cli.utils import SCIM_EXCEPTIONS
24
23
  from scim2_cli.utils import HeaderType
24
+ from scim2_cli.utils import escape_control_characters
25
25
  from scim2_cli.utils import exception_to_click_error
26
26
  from scim2_cli.utils import split_headers
27
27
 
28
28
 
29
- def load_config_files(
30
- schemas_fd, resource_types_fd, service_provider_config_fd
31
- ) -> tuple[
32
- list[type[Resource]], list[ResourceType] | None, ServiceProviderConfig | None
33
- ]:
34
- if schemas_fd:
35
- schemas_payload = json.load(schemas_fd)
36
- if isinstance(schemas_payload, dict):
37
- schemas_obj = ListResponse[Schema].model_validate(schemas_payload).resources
38
- else:
39
- schemas_obj = [Schema.model_validate(schema) for schema in schemas_payload]
40
- resource_models = [Resource.from_schema(schema) for schema in schemas_obj]
41
-
42
- else:
43
- resource_models = [User, Group]
44
-
45
- if resource_types_fd:
46
- resource_types_payload = json.load(resource_types_fd)
47
- if isinstance(schemas_payload, dict):
48
- resource_types = (
49
- ListResponse[ResourceType]
50
- .model_validate(resource_types_payload)
51
- .resources
52
- )
53
- else:
54
- resource_types = [
55
- ResourceType.model_validate(item) for item in resource_types_payload
56
- ]
57
- else:
58
- resource_types = None
59
-
60
- if service_provider_config_fd:
61
- spc_payload = json.load(service_provider_config_fd)
62
- service_provider_config = ServiceProviderConfig.model_validate(spc_payload)
63
- else:
64
- service_provider_config = None
65
-
66
- return resource_models, resource_types, service_provider_config
29
+ def load_objects(fd, model):
30
+ """Read a list of objects, bare or wrapped in a ListResponse, from a JSON file."""
31
+ payload = json.load(fd)
32
+ if isinstance(payload, dict):
33
+ return ListResponse[model].model_validate(payload).resources or []
34
+ return [model.model_validate(item) for item in payload]
35
+
36
+
37
+ def describe_server(
38
+ scim_client, schemas_fd, resource_types_fd, service_provider_config_fd
39
+ ) -> ScimProvider:
40
+ """Describe the server with the configuration files, and query it for the others."""
41
+ resource_types = (
42
+ load_objects(resource_types_fd, ResourceType)
43
+ if resource_types_fd
44
+ else scim_client.query(ResourceType).resources or []
45
+ )
46
+ schemas = (
47
+ load_objects(schemas_fd, Schema)
48
+ if schemas_fd
49
+ else scim_client.query(Schema).resources or []
50
+ )
51
+ config = (
52
+ ServiceProviderConfig.model_validate(json.load(service_provider_config_fd))
53
+ if service_provider_config_fd
54
+ else scim_client.query(ServiceProviderConfig)
55
+ )
56
+ return ScimProvider.from_discovery(schemas, resource_types, config)
67
57
 
68
58
 
69
59
  @click.group(cls=make_rst_to_ansi_formatter(DOC_URL, group=True))
@@ -73,7 +63,7 @@ def load_config_files(
73
63
  "--header",
74
64
  multiple=True,
75
65
  type=HeaderType(),
76
- help="Headers to pass in the HTTP requests. Can be passed multiple times.",
66
+ help="Headers to pass in the HTTP requests. Can be passed multiple times. Other users of the machine can see the command line arguments, so pass the secrets with the SCIM_CLI_HEADERS environment variable.",
77
67
  envvar="SCIM_CLI_HEADERS",
78
68
  )
79
69
  @click.option(
@@ -122,29 +112,21 @@ def cli(
122
112
  headers_dict = split_headers(header)
123
113
  client = Client(base_url=url, headers=headers_dict, verify=not no_verify)
124
114
 
125
- resource_models, resource_types_obj, spc_obj = load_config_files(
126
- schemas, resource_types, service_provider_config
127
- )
128
-
129
- scim_client = SyncSCIMClient(
130
- client,
131
- resource_models=resource_models,
132
- resource_types=resource_types_obj,
133
- service_provider_config=spc_obj,
134
- )
115
+ scim_client = SyncSCIMClient(client)
135
116
  try:
136
- scim_client.discover(
137
- schemas=not bool(schemas),
138
- resource_types=not bool(resource_types),
139
- service_provider_config=not bool(service_provider_config),
117
+ scim_client.provider = describe_server(
118
+ scim_client, schemas, resource_types, service_provider_config
140
119
  )
141
- except SCIMClientError as exc:
120
+ except (*SCIM_EXCEPTIONS, ScimProviderError) as exc:
142
121
  raise exception_to_click_error(exc) from exc
143
122
 
123
+ provider = scim_client.provider
144
124
  ctx.obj["client"] = scim_client
145
125
  ctx.obj["resource_models"] = {
146
- re.sub(r"\[.*\]", "", resource_model.__name__.lower()): resource_model
147
- for resource_model in ctx.obj["client"].resource_models
126
+ escape_control_characters(
127
+ re.sub(r"\[.*\]", "", resource_model.__name__.lower())
128
+ ): resource_model
129
+ for resource_model in map(provider.model_for, provider.resource_types)
148
130
  }
149
131
 
150
132
  if not click.get_text_stream("stdin").isatty(): # pragma: no cover
@@ -1,12 +1,13 @@
1
1
  import click
2
2
  from click import ClickException
3
3
  from pydanclick import from_pydantic
4
- from scim2_client import SCIMClientError
5
4
  from scim2_models import Context
6
5
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
7
6
 
8
7
  from scim2_cli.utils import DOC_URL
8
+ from scim2_cli.utils import SCIM_EXCEPTIONS
9
9
  from scim2_cli.utils import ModelCommand
10
+ from scim2_cli.utils import escape_options_help
10
11
  from scim2_cli.utils import exception_to_click_error
11
12
  from scim2_cli.utils import formatted_payload
12
13
  from scim2_cli.utils import unacceptable_fields
@@ -16,7 +17,7 @@ def create_payload(client, payload, indent):
16
17
  try:
17
18
  response = client.create(payload, raise_scim_errors=False)
18
19
 
19
- except SCIMClientError as scim_exc:
20
+ except SCIM_EXCEPTIONS as scim_exc:
20
21
  raise exception_to_click_error(scim_exc) from scim_exc
21
22
 
22
23
  payload = formatted_payload(response.model_dump(), indent)
@@ -75,7 +76,7 @@ def create_factory(model):
75
76
 
76
77
  create_payload(ctx.obj["client"], payload, indent)
77
78
 
78
- return create_command
79
+ return escape_options_help(create_command)
79
80
 
80
81
 
81
82
  @click.command(
@@ -1,6 +1,5 @@
1
1
  import click
2
2
  from click import ClickException
3
- from scim2_client import SCIMClientError
4
3
  from scim2_models import Message
5
4
  from scim2_models import Resource
6
5
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
@@ -8,6 +7,7 @@ from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
8
7
  from scim2_cli.utils import exception_to_click_error
9
8
 
10
9
  from .utils import DOC_URL
10
+ from .utils import SCIM_EXCEPTIONS
11
11
  from .utils import formatted_payload
12
12
 
13
13
 
@@ -39,7 +39,7 @@ def delete_cli(ctx, resource_type, id, indent):
39
39
  try:
40
40
  response = ctx.obj["client"].delete(resource_model, id, raise_scim_errors=False)
41
41
 
42
- except SCIMClientError as scim_exc:
42
+ except SCIM_EXCEPTIONS as scim_exc:
43
43
  raise exception_to_click_error(scim_exc) from scim_exc
44
44
 
45
45
  if response:
@@ -1,7 +1,7 @@
1
1
  import click
2
2
  from click import ClickException
3
- from scim2_client import SCIMClientError
4
3
  from scim2_models import ResourceType
4
+ from scim2_models import ResponseParameters
5
5
  from scim2_models import Schema
6
6
  from scim2_models import SearchRequest
7
7
  from scim2_models import ServiceProviderConfig
@@ -10,6 +10,7 @@ from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
10
10
  from scim2_cli.utils import exception_to_click_error
11
11
 
12
12
  from .utils import DOC_URL
13
+ from .utils import SCIM_EXCEPTIONS
13
14
  from .utils import formatted_payload
14
15
 
15
16
 
@@ -73,6 +74,9 @@ def query_cli(
73
74
  - If :code:`RESOURCE_TYPE` is :code:`user` and :code:`id` is not set, then the request will made on the :code:`/Users` endpoint.
74
75
  - If :code:`RESOURCE_TYPE` is not set, then the request will made on the :code:`/` endpoint.
75
76
 
77
+ When a single resource is queried, only :code:`--attribute` and :code:`--excluded-attribute` are
78
+ available, as defined in `RFC7644 §3.4.1 <https://www.rfc-editor.org/rfc/rfc7644#section-3.4.1>`_.
79
+
76
80
  Data passed in JSON format to stdin is sent as request arguments and all the other query arguments are ignored:
77
81
 
78
82
  .. code-block:: bash
@@ -92,10 +96,34 @@ def query_cli(
92
96
  f"Unknown resource type '{resource_type}. Available values are: {ok_values}'"
93
97
  ) from exc
94
98
 
99
+ single_resource = bool(id) or resource_type is ServiceProviderConfig
100
+ listing_options = [
101
+ name
102
+ for name, value in (
103
+ ("--start-index", start_index),
104
+ ("--count", count),
105
+ ("--filter", filter),
106
+ ("--sort-by", sort_by),
107
+ ("--sort-order", sort_order),
108
+ )
109
+ if value is not None
110
+ ]
111
+ if single_resource and listing_options:
112
+ raise ClickException(
113
+ f"{', '.join(listing_options)} cannot be used when querying a single resource."
114
+ )
115
+
95
116
  if ctx.obj.get("stdin"):
96
117
  check_request_payload = False
97
118
  payload = ctx.obj.get("stdin")
98
119
 
120
+ elif single_resource:
121
+ check_request_payload = True
122
+ payload = ResponseParameters(
123
+ attributes=attribute,
124
+ excluded_attributes=excluded_attribute,
125
+ )
126
+
99
127
  else:
100
128
  check_request_payload = True
101
129
  payload = SearchRequest(
@@ -112,12 +140,12 @@ def query_cli(
112
140
  response = ctx.obj["client"].query(
113
141
  resource_type,
114
142
  id,
115
- search_request=payload,
143
+ query_parameters=payload,
116
144
  check_request_payload=check_request_payload,
117
145
  raise_scim_errors=False,
118
146
  )
119
147
 
120
- except SCIMClientError as scim_exc:
148
+ except SCIM_EXCEPTIONS as scim_exc:
121
149
  raise exception_to_click_error(scim_exc) from scim_exc
122
150
 
123
151
  payload = formatted_payload(response.model_dump(), indent)
@@ -1,13 +1,14 @@
1
1
  import click
2
2
  from click import ClickException
3
3
  from pydanclick import from_pydantic
4
- from scim2_client import SCIMClientError
5
4
  from scim2_models import Context
6
5
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
7
6
 
7
+ from scim2_cli.utils import escape_options_help
8
8
  from scim2_cli.utils import exception_to_click_error
9
9
 
10
10
  from .utils import DOC_URL
11
+ from .utils import SCIM_EXCEPTIONS
11
12
  from .utils import ModelCommand
12
13
  from .utils import formatted_payload
13
14
  from .utils import unacceptable_fields
@@ -17,7 +18,7 @@ def replace_payload(client, payload, indent):
17
18
  try:
18
19
  response = client.replace(payload, raise_scim_errors=False)
19
20
 
20
- except SCIMClientError as scim_exc:
21
+ except SCIM_EXCEPTIONS as scim_exc:
21
22
  raise exception_to_click_error(scim_exc) from scim_exc
22
23
 
23
24
  payload = formatted_payload(response.model_dump(), indent)
@@ -82,7 +83,7 @@ def replace_factory(model):
82
83
  indent,
83
84
  )
84
85
 
85
- return replace_command
86
+ return escape_options_help(replace_command)
86
87
 
87
88
 
88
89
  @click.command(
@@ -1,11 +1,11 @@
1
1
  import click
2
- from scim2_client import SCIMClientError
3
2
  from scim2_models import SearchRequest
4
3
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
5
4
 
6
5
  from scim2_cli.utils import exception_to_click_error
7
6
 
8
7
  from .utils import DOC_URL
8
+ from .utils import SCIM_EXCEPTIONS
9
9
  from .utils import formatted_payload
10
10
 
11
11
 
@@ -91,7 +91,7 @@ def search_cli(
91
91
  raise_scim_errors=False,
92
92
  )
93
93
 
94
- except SCIMClientError as scim_exc:
94
+ except SCIM_EXCEPTIONS as scim_exc:
95
95
  raise exception_to_click_error(scim_exc) from scim_exc
96
96
 
97
97
  payload = formatted_payload(response.model_dump(), indent)
@@ -7,6 +7,7 @@ from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
7
7
 
8
8
  from .utils import DOC_URL
9
9
  from .utils import Color
10
+ from .utils import escape_control_characters
10
11
 
11
12
 
12
13
  @click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="test")
@@ -32,8 +33,8 @@ def test_cli(ctx, verbose, check_status_code, check_content_type):
32
33
  test
33
34
  """
34
35
  client = ctx.obj["client"]
35
- client.check_status_code = check_status_code
36
- client.check_content_type = check_content_type
36
+ client.check_response_status_codes = check_status_code
37
+ client.check_response_content_type = check_content_type
37
38
  results = check_server(client)
38
39
  click.echo(f"Performing a SCIM compliance check on {client.client.base_url} ...")
39
40
  success = True
@@ -43,11 +44,11 @@ def test_cli(ctx, verbose, check_status_code, check_content_type):
43
44
  else:
44
45
  status = click.style(result.status.name, fg=Color.red)
45
46
  success = False
46
- click.echo(f"{status} {result.title}")
47
+ click.echo(f"{status} {escape_control_characters(result.title)}")
47
48
 
48
49
  if result.reason:
49
- click.echo(f" {result.reason}")
50
+ click.echo(f" {escape_control_characters(result.reason)}")
50
51
  if verbose and result.data:
51
- click.echo(f" {result.data}")
52
+ click.echo(f" {escape_control_characters(result.data)}")
52
53
 
53
54
  sys.exit(0 if success else 1)
@@ -1,20 +1,28 @@
1
1
  import json
2
- import sys
3
- from enum import Enum
2
+ import re
3
+ from enum import StrEnum
4
4
 
5
5
  import click
6
+ from scim2_client import SCIMClientException
7
+ from scim2_models import SCIMException
6
8
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
7
9
 
8
10
  DOC_URL = "https://scim2-cli.readthedocs.io/"
9
11
  INDENTATION_SIZE = 4
10
12
 
13
+ SCIM_EXCEPTIONS = (SCIMClientException, SCIMException)
14
+
15
+ CONTROL_CHARACTERS = re.compile(
16
+ "[\x00-\x08\x0b-\x1f\x7f-\x9f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]"
17
+ )
18
+
11
19
 
12
20
  class HeaderType(click.ParamType):
13
21
  envvar_list_splitter = ";"
14
22
  name = "HEADER"
15
23
 
16
24
 
17
- class Color(str, Enum):
25
+ class Color(StrEnum):
18
26
  black = "black"
19
27
  red = "red"
20
28
  green = "green"
@@ -33,6 +41,25 @@ class Color(str, Enum):
33
41
  bright_white = "bright_white"
34
42
 
35
43
 
44
+ def escape_control_characters(text) -> str:
45
+ """Replace the control characters of a text by their escaped form.
46
+
47
+ The texts coming from the server would otherwise be interpreted by the
48
+ terminal as escape sequences.
49
+ """
50
+ return CONTROL_CHARACTERS.sub(
51
+ lambda match: match.group().encode("unicode_escape").decode(), str(text)
52
+ )
53
+
54
+
55
+ def escape_options_help(command):
56
+ """Escape the option help texts pydanclick takes from the schema descriptions."""
57
+ for param in command.params:
58
+ if param.help:
59
+ param.help = escape_control_characters(param.help)
60
+ return command
61
+
62
+
36
63
  def formatted_payload(obj, indent):
37
64
  indent = INDENTATION_SIZE if indent else False
38
65
  return json.dumps(obj, indent=indent)
@@ -111,8 +138,6 @@ def unacceptable_fields(context, model):
111
138
 
112
139
  def exception_to_click_error(exception):
113
140
  message = str(exception)
114
- if sys.version_info >= (3, 11) and hasattr(
115
- exception, "__notes__"
116
- ): # pragma: no cover
141
+ if hasattr(exception, "__notes__"):
117
142
  message += "\n" + "\n".join(exception.__notes__)
118
- return click.ClickException(message)
143
+ return click.ClickException(escape_control_characters(message))
File without changes
File without changes