Skip to content

Commit c802b70

Browse files
committed
refactor: use scim2-client to perform basic requests
1 parent 9f9debf commit c802b70

16 files changed

Lines changed: 1601 additions & 208 deletions

poetry.lock

Lines changed: 290 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ classifiers = [
2727
[tool.poetry.dependencies]
2828
python = "^3.10"
2929
click = "^8.1.7"
30-
requests = "^2.32.2"
30+
scim2-client = "^0.1.4"
3131
sphinx-click-rst-to-ansi-formatter = "^0.1.0"
3232

3333
[tool.poetry.group.doc]

scim2_cli/__init__.py

Lines changed: 33 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,49 @@
11
import json
22

33
import click
4-
import requests
4+
from httpx import Client
5+
from scim2_client import SCIMClient
6+
from scim2_models import EnterpriseUser
7+
from scim2_models import Group
8+
from scim2_models import User
59
from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
610

7-
DOC_URL = "https://scim2-cli.readthedocs.io/"
8-
BASE_HEADERS = {
9-
"Accept": "application/scim+json",
10-
"Content-Type": "application/scim+json",
11-
}
11+
from .create import create_cli
12+
from .delete import delete_cli
13+
from .query import query_cli
14+
from .replace import replace_cli
15+
from .search import search_cli
16+
from .utils import DOC_URL
1217

1318

1419
@click.group(cls=make_rst_to_ansi_formatter(DOC_URL, group=True))
1520
@click.argument("url")
1621
@click.pass_context
1722
def cli(ctx, url):
1823
"""SCIM application development CLI."""
24+
1925
ctx.ensure_object(dict)
2026
ctx.obj["URL"] = url
21-
22-
if not click.get_text_stream("stdin").isatty(): # pragma: no cover
23-
stdin = click.get_text_stream("stdin").read().strip()
24-
ctx.obj["STDIN"] = json.loads(stdin)
25-
26-
27-
@cli.command(cls=make_rst_to_ansi_formatter(DOC_URL))
28-
@click.pass_context
29-
def get(ctx):
30-
"""Perform a `SCIM GET <https://www.rfc-editor.org/rfc/rfc7644#section-3.4.1>`_ request.
31-
32-
Data passed in JSON format to stdin is sent as request arguments:
33-
34-
.. code-block:: bash
35-
36-
echo '{"foo": "bar"}' | scim https://scim.example get
37-
"""
38-
39-
response = requests.get(
40-
ctx.obj["URL"],
41-
params=ctx.obj.get("STDIN"),
42-
headers=BASE_HEADERS,
43-
allow_redirects=True,
44-
)
45-
click.echo(response.text)
46-
47-
48-
@cli.command(cls=make_rst_to_ansi_formatter(DOC_URL))
49-
@click.pass_context
50-
def post(ctx):
51-
"""Perform a `SCIM POST <https://www.rfc-editor.org/rfc/rfc7644#section-3.3>`_ request.
52-
53-
Data passed in JSON format to stdin is sent as request payload:
54-
55-
.. code-block:: bash
56-
57-
echo '{"foo": "bar"}' | scim https://scim.example post
58-
"""
59-
60-
response = requests.post(
61-
ctx.obj["URL"],
62-
json=ctx.obj.get("STDIN"),
63-
headers=BASE_HEADERS,
64-
allow_redirects=True,
65-
)
66-
click.echo(response.text)
67-
68-
69-
@cli.command(cls=make_rst_to_ansi_formatter(DOC_URL))
70-
@click.pass_context
71-
def put(ctx):
72-
"""Perform a `SCIM PUT <https://www.rfc-editor.org/rfc/rfc7644#section-3.5.1>`_ request.
73-
74-
Data passed in JSON format to stdin is sent as request payload:
75-
76-
.. code-block:: bash
77-
78-
echo '{"foo": "bar"}' | scim https://scim.example put
79-
"""
80-
81-
response = requests.put(
82-
ctx.obj["URL"],
83-
json=ctx.obj.get("STDIN"),
84-
headers=BASE_HEADERS,
85-
allow_redirects=True,
27+
client = Client(base_url=ctx.obj["URL"])
28+
ctx.obj["client"] = SCIMClient(
29+
client, resource_types=(User, User[EnterpriseUser], Group)
8630
)
87-
click.echo(response.text)
88-
89-
90-
@cli.command(cls=make_rst_to_ansi_formatter(DOC_URL))
91-
@click.pass_context
92-
def patch(ctx):
93-
"""Perform a `SCIM PATCH <https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2>`_ request.
94-
95-
Data passed in JSON format to stdin is sent as request payload:
96-
97-
.. code-block:: bash
98-
99-
echo '{"foo": "bar"}' | scim https://scim.example patch
100-
"""
31+
ctx.obj["resource_types"] = {
32+
resource_type.__name__.lower(): resource_type
33+
for resource_type in ctx.obj["client"].resource_types
34+
}
10135

102-
response = requests.patch(
103-
ctx.obj["URL"],
104-
json=ctx.obj.get("STDIN"),
105-
headers=BASE_HEADERS,
106-
allow_redirects=True,
107-
)
108-
click.echo(response.text)
109-
110-
111-
@cli.command(cls=make_rst_to_ansi_formatter(DOC_URL))
112-
@click.pass_context
113-
def delete(ctx):
114-
"""Perform a `SCIM DELETE <https://www.rfc-editor.org/rfc/rfc7644#section-3.6>`_ request.
115-
116-
Data passed in JSON format to stdin is sent as request payload:
117-
118-
.. code-block:: bash
119-
120-
echo '{"foo": "bar"}' | scim https://scim.example delete
121-
"""
122-
123-
response = requests.delete(
124-
ctx.obj["URL"],
125-
json=ctx.obj.get("STDIN"),
126-
headers=BASE_HEADERS,
127-
allow_redirects=True,
128-
)
129-
click.echo(response.text)
36+
if not click.get_text_stream("stdin").isatty(): # pragma: no cover
37+
if stdin := click.get_text_stream("stdin").read().strip():
38+
try:
39+
ctx.obj["stdin"] = json.loads(stdin)
40+
except json.JSONDecodeError as exc:
41+
message = f"Invalid JSON input.\n{exc}"
42+
raise click.ClickException(message) from exc
43+
44+
45+
cli.add_command(create_cli)
46+
cli.add_command(query_cli)
47+
cli.add_command(replace_cli)
48+
cli.add_command(delete_cli)
49+
cli.add_command(search_cli)

scim2_cli/create.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import click
2+
import httpx
3+
from click import ClickException
4+
from pydantic import ValidationError
5+
from scim2_client import SCIMClientError
6+
from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
7+
8+
from .utils import DOC_URL
9+
from .utils import formatted_payload
10+
11+
12+
@click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="create")
13+
@click.pass_context
14+
@click.option(
15+
"--indent/--no-indent",
16+
is_flag=True,
17+
default=True,
18+
help="Indent JSON response payloads.",
19+
)
20+
def create_cli(ctx, indent):
21+
"""Perform a `SCIM POST <https://www.rfc-editor.org/rfc/rfc7644#section-3.3>`_ request
22+
on resources endpoint.
23+
24+
Input data is expected to be passed in JSON format to stdin:
25+
26+
.. code-block:: bash
27+
28+
echo '{"userName": "bjensen@example.com", "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"]}' | scim https://scim.example create user
29+
30+
"""
31+
32+
payload = ctx.obj.get("stdin")
33+
if not payload:
34+
raise ClickException("Input data is missing")
35+
36+
try:
37+
response = ctx.obj["client"].create(payload)
38+
39+
except (httpx.HTTPError, SCIMClientError) as exc:
40+
raise ClickException(exc) from exc
41+
42+
except ValidationError as exc:
43+
payload = formatted_payload(exc.response_payload, indent)
44+
message = f"The server response is invalid:\n{payload}\n{exc}"
45+
raise ClickException(message) from exc
46+
47+
payload = formatted_payload(response.model_dump(), indent)
48+
click.echo(payload)

scim2_cli/delete.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import click
2+
import httpx
3+
from click import ClickException
4+
from scim2_client import SCIMClientError
5+
from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
6+
7+
from .utils import DOC_URL
8+
from .utils import formatted_payload
9+
10+
11+
@click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="delete")
12+
@click.argument("resource-type", required=True)
13+
@click.argument("id", required=True)
14+
@click.option(
15+
"--indent/--no-indent",
16+
is_flag=True,
17+
default=True,
18+
help="Indent JSON response payloads.",
19+
)
20+
@click.pass_context
21+
def delete_cli(ctx, resource_type, id, indent):
22+
"""Perform a `SCIM DELETE query <https://www.rfc-editor.org/rfc/rfc7644#section-3.6>`_ request.
23+
24+
.. code-block:: bash
25+
26+
scim https://scim.example delete user 1234
27+
"""
28+
29+
try:
30+
resource_type = ctx.obj["resource_types"][resource_type]
31+
except KeyError:
32+
ok_values = ", ".join(ctx.obj["resource_types"])
33+
raise ClickException(
34+
f"Unknown resource type '{resource_type}'. Available values are: {ok_values}'"
35+
)
36+
37+
try:
38+
response = ctx.obj["client"].delete(resource_type, id)
39+
40+
except (httpx.HTTPError, SCIMClientError) as exc:
41+
raise ClickException(exc) from exc
42+
43+
if response:
44+
payload = formatted_payload(response.model_dump(), indent)
45+
click.echo(payload)

scim2_cli/query.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from typing import List
2+
from typing import Optional
3+
4+
import click
5+
import httpx
6+
from click import ClickException
7+
from pydantic import ValidationError
8+
from scim2_client import SCIMClientError
9+
from scim2_models import SearchRequest
10+
from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
11+
12+
from .utils import DOC_URL
13+
from .utils import formatted_payload
14+
15+
16+
@click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="query")
17+
@click.pass_context
18+
@click.argument("resource_type", required=False)
19+
@click.argument("id", required=False)
20+
@click.option(
21+
"--attribute",
22+
multiple=True,
23+
help="A multi-valued list of strings indicating the names of resource attributes to return in the response, overriding the set of attributes that would be returned by default.",
24+
)
25+
@click.option(
26+
"--excluded-attribute",
27+
multiple=True,
28+
help="A multi-valued list of strings indicating the names of resource attributes to be removed from the default set of attributes to return.",
29+
)
30+
@click.option(
31+
"--start-index",
32+
type=int,
33+
help="An integer indicating the 1-based index of the first query result.",
34+
)
35+
@click.option(
36+
"--count",
37+
type=int,
38+
help="An integer indicating the desired maximum number of query results per page.",
39+
)
40+
@click.option(
41+
"--filter", help="The filter string used to request a subset of resources."
42+
)
43+
@click.option(
44+
"--sort-by",
45+
help="A string indicating the attribute whose value SHALL be used to order the returned responses.",
46+
)
47+
@click.option(
48+
"--sort-order",
49+
help="A string indicating the order in which the “sortBy” parameter is applied.",
50+
)
51+
@click.option(
52+
"--indent/--no-indent",
53+
is_flag=True,
54+
default=True,
55+
help="Indent JSON response payloads.",
56+
)
57+
def query_cli(
58+
ctx,
59+
resource_type: Optional[str],
60+
id: Optional[str],
61+
attribute: List[str],
62+
excluded_attribute: List[str],
63+
start_index: int,
64+
count: int,
65+
filter: str,
66+
sort_by: str,
67+
sort_order: str,
68+
indent: bool,
69+
):
70+
"""Perform a `SCIM GET <https://www.rfc-editor.org/rfc/rfc7644#section-3.4.1>`_ request
71+
on the :code:`RESOURCE_TYPE` endpoint.
72+
73+
- If :code:`RESOURCE_TYPE` is :code:`user` and :code:`id` is `1234`, then the request will made on the :code:`/Users/1234` endpoint.
74+
- If :code:`RESOURCE_TYPE` is :code:`user` and :code:`id` is not set, then the request will made on the :code:`/Users` endpoint.
75+
- If :code:`RESOURCE_TYPE` is not set, then the request will made on the :code:`/` endpoint.
76+
77+
Data passed in JSON format to stdin is sent as request arguments and all the other query arguments are ignored:
78+
79+
.. code-block:: bash
80+
81+
echo '{"startIndex": 50, "count": 10}' | scim https://scim.example query user
82+
"""
83+
84+
if resource_type:
85+
try:
86+
resource_type = ctx.obj["resource_types"][resource_type]
87+
except KeyError:
88+
ok_values = ", ".join(ctx.obj["resource_types"])
89+
raise ClickException(
90+
f"Unknown resource type '{resource_type}. Available values are: {ok_values}'"
91+
)
92+
93+
if ctx.obj.get("stdin"):
94+
check_request_payload = False
95+
payload = ctx.obj.get("stdin")
96+
97+
else:
98+
check_request_payload = True
99+
payload = SearchRequest(
100+
attributes=attribute,
101+
excluded_attributes=excluded_attribute,
102+
start_index=start_index,
103+
count=count,
104+
filter=filter,
105+
sort_by=sort_by,
106+
sort_order=sort_order,
107+
)
108+
109+
try:
110+
if resource_type:
111+
response = ctx.obj["client"].query(
112+
resource_type,
113+
id,
114+
search_request=payload,
115+
check_request_payload=check_request_payload,
116+
)
117+
118+
else:
119+
response = ctx.obj["client"].query_all(
120+
search_request=payload, check_request_payload=check_request_payload
121+
)
122+
123+
except (httpx.HTTPError, SCIMClientError) as exc:
124+
raise ClickException(exc) from exc
125+
126+
except ValidationError as exc:
127+
payload = formatted_payload(exc.response_payload, indent)
128+
message = f"The server response is invalid:\n{payload}\n{exc}"
129+
raise ClickException(message) from exc
130+
131+
payload = formatted_payload(response.model_dump(), indent)
132+
click.echo(payload)

0 commit comments

Comments
 (0)