Skip to content

Commit 36c487a

Browse files
committed
feat: dynamic options for create and replace commands
1 parent e2e8409 commit 36c487a

11 files changed

Lines changed: 569 additions & 57 deletions

File tree

README.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@ Check the [reference](https://scim2-cli.readthedocs.io/en/latest/reference.html)
2424
Here is an example of resource creation:
2525

2626
```shell
27-
$ scim2 https://auth.example --headers "Authorization: Bearer 12345" create user << EOL
28-
{
29-
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
30-
"userName": "bjensen@example.com"
31-
}
32-
EOL
27+
$ scim2 https://auth.example --headers "Authorization: Bearer 12345" create user --user-name "bjensen@example.com"
3328
{
3429
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
3530
"id": "2819c223-7f76-453a-919d-413861904646",

doc/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
[0.2.0] - Unreleased
5+
--------------------
6+
7+
Fixed
8+
^^^^^
9+
- Dynamic command parameters for ``create`` and ``replace``.
10+
411
[0.1.4] - 2024-07-26
512
--------------------
613

poetry.lock

Lines changed: 22 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ click = "^8.1.7"
3434
scim2-client = "^0.1.10"
3535
scim2-tester = "^0.1.3"
3636
sphinx-click-rst-to-ansi-formatter = "^0.1.0"
37+
pydanclick = "^0.3.0"
3738

3839
[tool.poetry.group.doc]
3940
optional = true

scim2_cli/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,5 @@ def cli(ctx, url):
4747
cli.add_command(search_cli)
4848
cli.add_command(test_cli)
4949

50-
5150
if __name__ == "__main__": # pragma: no cover
5251
cli()

scim2_cli/create.py

Lines changed: 97 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,97 @@
22

33
import click
44
from click import ClickException
5+
from pydanclick import from_pydantic
56
from scim2_client import SCIMClientError
7+
from scim2_models import Context
68
from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
79

810
from .utils import DOC_URL
11+
from .utils import ModelCommand
912
from .utils import formatted_payload
1013
from .utils import split_headers
14+
from .utils import unacceptable_fields
1115

1216

13-
@click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="create")
17+
def create_payload(client, payload, indent, headers):
18+
try:
19+
response = client.create(payload, headers=split_headers(headers))
20+
21+
except SCIMClientError as scim_exc:
22+
message = str(scim_exc)
23+
if sys.version_info >= (3, 11) and hasattr(
24+
scim_exc, "__notes__"
25+
): # pragma: no cover
26+
for note in scim_exc.__notes__:
27+
message = f"{message}\n{note}"
28+
raise ClickException(message) from scim_exc
29+
30+
payload = formatted_payload(response.model_dump(), indent)
31+
click.echo(payload)
32+
33+
34+
def create_factory(model):
35+
exclude = unacceptable_fields(Context.RESOURCE_CREATION_REQUEST, model)
36+
37+
@click.command(
38+
cls=make_rst_to_ansi_formatter(DOC_URL),
39+
name=model.__name__.lower(),
40+
)
41+
@click.option(
42+
"--indent/--no-indent",
43+
is_flag=True,
44+
default=True,
45+
help="Indent JSON response payloads.",
46+
)
47+
@click.option(
48+
"-h", "--headers", multiple=True, help="Header to pass in the HTTP requests."
49+
)
50+
@from_pydantic("obj", model, exclude=exclude)
51+
@click.pass_context
52+
def create_command(ctx, indent, headers, obj: model, *args, **kwargs):
53+
"""Perform a `SCIM POST <https://www.rfc-editor.org/rfc/rfc7644#section-3.3>`_ request
54+
on resources endpoint.
55+
56+
Input data can be passed through parameters like :code:`--external-id`.
57+
58+
.. code-block:: bash
59+
60+
scim https://scim.example create user --user-name "foo" --name-given-name "bar"
61+
62+
Multiple attributes should be passed as JSON payloads:
63+
64+
.. code-block:: bash
65+
66+
scim https://scim.example create user \\
67+
--user-name "foo" \\
68+
--emails '[{"value":"foo@bar.example", "primary": true}, {"value": "foo@baz.example"}]'
69+
70+
Input can also be passed through stdin in JSON format:
71+
72+
.. code-block:: bash
73+
74+
echo '{"userName": "bjensen@example.com", "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"]}' | scim https://scim.example create user
75+
"""
76+
77+
if obj == model():
78+
obj = None
79+
80+
payload = ctx.obj.get("stdin") or obj
81+
if not payload:
82+
click.echo(ctx.get_help())
83+
ctx.exit(1)
84+
85+
create_payload(ctx.obj["client"], payload, indent, headers)
86+
87+
return create_command
88+
89+
90+
@click.command(
91+
cls=ModelCommand,
92+
factory=create_factory,
93+
name="create",
94+
invoke_without_command=True,
95+
)
1496
@click.pass_context
1597
@click.option(
1698
"--indent/--no-indent",
@@ -25,29 +107,26 @@ def create_cli(ctx, indent, headers):
25107
"""Perform a `SCIM POST <https://www.rfc-editor.org/rfc/rfc7644#section-3.3>`_ request
26108
on resources endpoint.
27109
28-
Input data is expected to be passed in JSON format to stdin:
110+
There are subcommands for all the available models, with dynamic attributes.
111+
See the attributes for :code:`user` with:
29112
30113
.. code-block:: bash
31114
32-
echo '{"userName": "bjensen@example.com", "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"]}' | scim https://scim.example create user
115+
scim https://scim.example create user --help
116+
117+
If no subcommand is executed, input data is expected to be passed in JSON format to stdin:
118+
119+
.. code-block:: bash
33120
121+
echo '{"userName": "bjensen@example.com", "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"]}' | scim https://scim.example create
34122
"""
35123

124+
if ctx.invoked_subcommand is not None:
125+
return
126+
36127
payload = ctx.obj.get("stdin")
37128
if not payload:
38-
raise ClickException("Input data is missing")
39-
40-
try:
41-
response = ctx.obj["client"].create(payload, headers=split_headers(headers))
42-
43-
except SCIMClientError as scim_exc:
44-
message = str(scim_exc)
45-
if sys.version_info >= (3, 11) and hasattr(
46-
scim_exc, "__notes__"
47-
): # pragma: no cover
48-
for note in scim_exc.__notes__:
49-
message = f"{message}\n{note}"
50-
raise ClickException(message) from scim_exc
129+
click.echo(ctx.get_help())
130+
ctx.exit(1)
51131

52-
payload = formatted_payload(response.model_dump(), indent)
53-
click.echo(payload)
132+
create_payload(ctx.obj["client"], payload, indent, headers)

scim2_cli/replace.py

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,99 @@
22

33
import click
44
from click import ClickException
5+
from pydanclick import from_pydantic
56
from scim2_client import SCIMClientError
7+
from scim2_models import Context
68
from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
79

810
from .utils import DOC_URL
11+
from .utils import ModelCommand
912
from .utils import formatted_payload
1013
from .utils import split_headers
14+
from .utils import unacceptable_fields
1115

1216

13-
@click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="replace")
17+
def replace_payload(client, payload, indent, headers):
18+
try:
19+
response = client.replace(payload, headers=split_headers(headers))
20+
21+
except SCIMClientError as scim_exc:
22+
message = str(scim_exc)
23+
if sys.version_info >= (3, 11) and hasattr(
24+
scim_exc, "__notes__"
25+
): # pragma: no cover
26+
for note in scim_exc.__notes__:
27+
message = f"{message}\n{note}"
28+
raise ClickException(message) from scim_exc
29+
30+
payload = formatted_payload(response.model_dump(), indent)
31+
click.echo(payload)
32+
33+
34+
def replace_factory(model):
35+
exclude = unacceptable_fields(Context.RESOURCE_REPLACEMENT_REQUEST, model)
36+
exclude.remove("id")
37+
38+
@click.command(
39+
cls=make_rst_to_ansi_formatter(DOC_URL),
40+
name=model.__name__.lower(),
41+
)
42+
@click.option(
43+
"--indent/--no-indent",
44+
is_flag=True,
45+
default=True,
46+
help="Indent JSON response payloads.",
47+
)
48+
@click.option(
49+
"-h", "--headers", multiple=True, help="Header to pass in the HTTP requests."
50+
)
51+
@from_pydantic("obj", model, exclude=exclude)
52+
@click.pass_context
53+
def replace_command(ctx, indent, headers, obj: model, *args, **kwargs):
54+
"""Perform a `SCIM PUT <https://www.rfc-editor.org/rfc/rfc7644#section-3.3>`_ request
55+
on resources endpoint.
56+
57+
Input data can be passed through parameters like :code:`--external-id`.
58+
59+
.. code-block:: bash
60+
61+
scim https://scim.example replace user --id "xxxx-yyyy" --user-name "foo" --name-given-name "bar"
62+
63+
Multiple attributes should be passed as JSON payloads:
64+
65+
.. code-block:: bash
66+
67+
scim https://scim.example replace user \\
68+
--id "xxxx-yyyy" \\
69+
--user-name "foo" \\
70+
--emails '[{"value":"foo@bar.example", "primary": true}, {"value": "foo@baz.example"}]'
71+
72+
Input can also be passed through stdin in JSON format:
73+
74+
.. code-block:: bash
75+
76+
echo '{"id": "xxxx-yyyy", "userName": "bjensen@example.com", "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"]}' | scim https://scim.example replace user
77+
"""
78+
79+
if obj == model():
80+
obj = None
81+
82+
payload = ctx.obj.get("stdin") or obj
83+
if not payload:
84+
click.echo(ctx.get_help())
85+
ctx.exit(1)
86+
87+
replace_payload(ctx.obj["client"], payload, indent, headers)
88+
89+
return replace_command
90+
91+
92+
@click.command(
93+
cls=ModelCommand,
94+
factory=replace_factory,
95+
name="replace",
96+
invoke_without_command=True,
97+
)
1498
@click.pass_context
1599
@click.option(
16100
"-h", "--headers", multiple=True, help="Header to pass in the HTTP requests."
@@ -25,29 +109,26 @@ def replace_cli(ctx, headers, indent):
25109
"""Perform a `SCIM PUT <https://www.rfc-editor.org/rfc/rfc7644#section-3.5.1>`_ request
26110
on the resources endpoint.
27111
28-
Input data is expected to be passed in JSON format to stdin:
112+
There are subcommands for all the available models, with dynamic attributes.
113+
See the attributes for :code:`user` with:
114+
115+
.. code-block:: bash
116+
117+
scim https://scim.example replace user --help
118+
119+
If no subcommand is executed, input data is expected to be passed in JSON format to stdin:
29120
30121
.. code-block:: bash
31122
32123
echo '{"userName": "bjensen@example.com", "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "id": "1234"}' | scim https://scim.example replace user
33124
34125
"""
126+
if ctx.invoked_subcommand is not None:
127+
return
35128

36129
payload = ctx.obj.get("stdin")
37130
if not payload:
38-
raise ClickException("Input data is missing")
39-
40-
try:
41-
response = ctx.obj["client"].replace(payload, headers=split_headers(headers))
42-
43-
except SCIMClientError as scim_exc:
44-
message = str(scim_exc)
45-
if sys.version_info >= (3, 11) and hasattr(
46-
scim_exc, "__notes__"
47-
): # pragma: no cover
48-
for note in scim_exc.__notes__:
49-
message = f"{message}\n{note}"
50-
raise ClickException(message) from scim_exc
131+
click.echo(ctx.get_help())
132+
ctx.exit(1)
51133

52-
payload = formatted_payload(response.model_dump(), indent)
53-
click.echo(payload)
134+
replace_payload(ctx.obj["client"], payload, indent, headers)

0 commit comments

Comments
 (0)