scim2-cli 0.4.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scim2-cli
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: SCIM application development CLI
5
5
  Keywords: scim,scim2,provisioning,rfc7643,rfc7644
6
6
  Author: Yaal Coop
@@ -218,9 +218,9 @@ Classifier: Environment :: Web Environment
218
218
  Classifier: Programming Language :: Python
219
219
  Classifier: Operating System :: OS Independent
220
220
  Requires-Dist: click>=8.1.7
221
- Requires-Dist: scim2-client[httpx2]>=0.9.0
221
+ Requires-Dist: scim2-client[httpx2]>=0.11.2
222
222
  Requires-Dist: scim2-tester>=0.4.0
223
- Requires-Dist: sphinx-click-rst-to-ansi-formatter>=0.1.0
223
+ Requires-Dist: sphinx-click-rst-to-ansi-formatter>=0.2.0
224
224
  Requires-Dist: pydanclick>=0.5.0
225
225
  Requires-Dist: pygments>=2.18.0
226
226
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-cli"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "SCIM application development CLI"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -30,9 +30,9 @@ classifiers = [
30
30
  requires-python = ">= 3.11"
31
31
  dependencies = [
32
32
  "click>=8.1.7",
33
- "scim2-client[httpx2]>=0.9.0",
33
+ "scim2-client[httpx2]>=0.11.2",
34
34
  "scim2-tester>=0.4.0",
35
- "sphinx-click-rst-to-ansi-formatter>=0.1.0",
35
+ "sphinx-click-rst-to-ansi-formatter>=0.2.0",
36
36
  "pydanclick>=0.5.0",
37
37
  "pygments>=2.18.0",
38
38
  ]
@@ -56,7 +56,7 @@ scim2 = "scim2_cli:cli"
56
56
 
57
57
  [dependency-groups]
58
58
  dev = [
59
- "mypy>=1.13.0",
59
+ "mypy>=2.3.1",
60
60
  "prek>=0.1.0",
61
61
  "pytest>=8.2.1",
62
62
  "pytest-cov>=6.0.0",
@@ -85,6 +85,7 @@ scim2-server = false
85
85
  scim2-cli = false
86
86
  scim2-tester = false
87
87
  pytest-scim2-server = false
88
+ sphinx-click-rst-to-ansi-formatter = false
88
89
 
89
90
  [tool.uv.build-backend]
90
91
  module-root = ""
@@ -104,6 +105,7 @@ exclude_lines = [
104
105
  "raise NotImplementedError",
105
106
  "except ImportError",
106
107
  "if app.debug",
108
+ "if TYPE_CHECKING:",
107
109
  ]
108
110
 
109
111
  [tool.ruff.lint]
@@ -141,6 +143,12 @@ ignore-words-list = "datas"
141
143
 
142
144
  [tool.mypy]
143
145
  plugins = ["pydantic.mypy"]
146
+ files = ["scim2_cli"]
147
+ strict = true
148
+
149
+ [tool.pydantic-mypy]
150
+ init_forbid_extra = true
151
+ init_typed = true
144
152
 
145
153
  [tool.pytest.ini_options]
146
154
  testpaths = ["tests"]
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-cli"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "SCIM application development CLI"
9
9
  authors = [{name="Yaal Coop", email="contact@yaal.coop"}]
10
10
  license = {file = "LICENSE.md"}
@@ -27,9 +27,9 @@ classifiers = [
27
27
  requires-python = ">= 3.11"
28
28
  dependencies = [
29
29
  "click>=8.1.7",
30
- "scim2-client[httpx2]>=0.9.0",
30
+ "scim2-client[httpx2]>=0.11.2",
31
31
  "scim2-tester>=0.4.0",
32
- "sphinx-click-rst-to-ansi-formatter>=0.1.0",
32
+ "sphinx-click-rst-to-ansi-formatter>=0.2.0",
33
33
  "pydanclick>=0.5.0",
34
34
  "pygments>=2.18.0",
35
35
  ]
@@ -42,7 +42,7 @@ funding = "https://github.com/sponsors/python-scim"
42
42
 
43
43
  [dependency-groups]
44
44
  dev = [
45
- "mypy>=1.13.0",
45
+ "mypy>=2.3.1",
46
46
  "prek>=0.1.0",
47
47
  "pytest>=8.2.1",
48
48
  "pytest-cov>=6.0.0",
@@ -69,7 +69,8 @@ scim2 = "scim2_cli:cli"
69
69
 
70
70
  [tool.uv]
71
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 }
72
+ # TODO: remove sphinx-click-rst-to-ansi-formatter after 2026-10-20, when 0.2.0 is older than 14 days.
73
+ exclude-newer-package = { scim2-models = false, scim2-client = false, scim2-server = false, scim2-cli = false, scim2-tester = false, pytest-scim2-server = false, sphinx-click-rst-to-ansi-formatter = false }
73
74
 
74
75
  [tool.uv.build-backend]
75
76
  module-root = ""
@@ -89,6 +90,7 @@ exclude_lines = [
89
90
  "raise NotImplementedError",
90
91
  "except ImportError",
91
92
  "if app.debug",
93
+ "if TYPE_CHECKING:",
92
94
  ]
93
95
 
94
96
  [tool.ruff.lint]
@@ -128,6 +130,12 @@ ignore-words-list = "datas"
128
130
  plugins = [
129
131
  "pydantic.mypy"
130
132
  ]
133
+ files = ["scim2_cli"]
134
+ strict = true
135
+
136
+ [tool.pydantic-mypy]
137
+ init_forbid_extra = true
138
+ init_typed = true
131
139
 
132
140
  [tool.pytest.ini_options]
133
141
  testpaths = ["tests"]
@@ -1,10 +1,16 @@
1
1
  import json
2
2
  import re
3
+ import sys
4
+ from typing import IO
5
+ from typing import Any
6
+ from typing import TypeVar
7
+ from typing import cast
3
8
 
4
9
  import click
5
10
  from httpx2 import Client
6
11
  from scim2_client.engines.httpx2 import SyncSCIMClient
7
12
  from scim2_models import ListResponse
13
+ from scim2_models import Resource
8
14
  from scim2_models import ResourceType
9
15
  from scim2_models import Schema
10
16
  from scim2_models import ScimProvider
@@ -25,33 +31,44 @@ from scim2_cli.utils import escape_control_characters
25
31
  from scim2_cli.utils import exception_to_click_error
26
32
  from scim2_cli.utils import split_headers
27
33
 
34
+ ResourceT = TypeVar("ResourceT", bound=Resource[Any])
28
35
 
29
- def load_objects(fd, model):
36
+
37
+ def load_objects(fd: IO[str], model: type[ResourceT]) -> list[ResourceT]:
30
38
  """Read a list of objects, bare or wrapped in a ListResponse, from a JSON file."""
31
39
  payload = json.load(fd)
32
40
  if isinstance(payload, dict):
33
- return ListResponse[model].model_validate(payload).resources or []
41
+ list_response: ListResponse[ResourceT] = cast(Any, ListResponse)[
42
+ model
43
+ ].model_validate(payload)
44
+ return list_response.resources or []
34
45
  return [model.model_validate(item) for item in payload]
35
46
 
36
47
 
37
48
  def describe_server(
38
- scim_client, schemas_fd, resource_types_fd, service_provider_config_fd
49
+ scim_client: SyncSCIMClient,
50
+ schemas_fd: IO[str] | None,
51
+ resource_types_fd: IO[str] | None,
52
+ service_provider_config_fd: IO[str] | None,
39
53
  ) -> ScimProvider:
40
54
  """Describe the server with the configuration files, and query it for the others."""
41
55
  resource_types = (
42
56
  load_objects(resource_types_fd, ResourceType)
43
57
  if resource_types_fd
44
- else scim_client.query(ResourceType).resources or []
58
+ else cast(
59
+ "ListResponse[ResourceType]", scim_client.query(ResourceType)
60
+ ).resources
61
+ or []
45
62
  )
46
63
  schemas = (
47
64
  load_objects(schemas_fd, Schema)
48
65
  if schemas_fd
49
- else scim_client.query(Schema).resources or []
66
+ else cast("ListResponse[Schema]", scim_client.query(Schema)).resources or []
50
67
  )
51
68
  config = (
52
69
  ServiceProviderConfig.model_validate(json.load(service_provider_config_fd))
53
70
  if service_provider_config_fd
54
- else scim_client.query(ServiceProviderConfig)
71
+ else cast(ServiceProviderConfig, scim_client.query(ServiceProviderConfig))
55
72
  )
56
73
  return ScimProvider.from_discovery(schemas, resource_types, config)
57
74
 
@@ -95,14 +112,14 @@ def describe_server(
95
112
  )
96
113
  @click.pass_context
97
114
  def cli(
98
- ctx,
99
- url: str,
115
+ ctx: click.Context,
116
+ url: str | None,
100
117
  header: list[str],
101
- no_verify,
102
- schemas,
103
- resource_types,
104
- service_provider_config,
105
- ):
118
+ no_verify: bool,
119
+ schemas: IO[str] | None,
120
+ resource_types: IO[str] | None,
121
+ service_provider_config: IO[str] | None,
122
+ ) -> None:
106
123
  """SCIM application development CLI."""
107
124
  ctx.ensure_object(dict)
108
125
 
@@ -122,15 +139,15 @@ def cli(
122
139
 
123
140
  provider = scim_client.provider
124
141
  ctx.obj["client"] = scim_client
125
- ctx.obj["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)
130
- }
131
-
132
- if not click.get_text_stream("stdin").isatty(): # pragma: no cover
133
- if stdin := click.get_text_stream("stdin").read().strip():
142
+ ctx.obj["resource_models"] = {}
143
+ for resource_type in provider.resource_types:
144
+ resource_model = provider.model_for(resource_type)
145
+ assert resource_model is not None
146
+ name = re.sub(r"\[.*\]", "", resource_model.__name__.lower())
147
+ ctx.obj["resource_models"][escape_control_characters(name)] = resource_model
148
+
149
+ if not sys.stdin.isatty(): # pragma: no cover
150
+ if stdin := sys.stdin.read().strip():
134
151
  try:
135
152
  ctx.obj["stdin"] = json.loads(stdin)
136
153
  except json.JSONDecodeError as exc:
@@ -1,7 +1,13 @@
1
+ from typing import Any
2
+ from typing import cast
3
+
1
4
  import click
2
5
  from click import ClickException
3
6
  from pydanclick import from_pydantic
7
+ from scim2_client.engines.httpx2 import SyncSCIMClient
4
8
  from scim2_models import Context
9
+ from scim2_models import Error
10
+ from scim2_models import Resource
5
11
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
6
12
 
7
13
  from scim2_cli.utils import DOC_URL
@@ -13,18 +19,22 @@ from scim2_cli.utils import formatted_payload
13
19
  from scim2_cli.utils import unacceptable_fields
14
20
 
15
21
 
16
- def create_payload(client, payload, indent):
22
+ def create_payload(
23
+ client: SyncSCIMClient, payload: Resource[Any] | dict[str, Any], indent: bool
24
+ ) -> None:
17
25
  try:
18
- response = client.create(payload, raise_scim_errors=False)
26
+ # Response payloads are always checked, so the client never returns a dict.
27
+ response = cast(
28
+ "Resource[Any] | Error", client.create(payload, raise_scim_errors=False)
29
+ )
19
30
 
20
31
  except SCIM_EXCEPTIONS as scim_exc:
21
32
  raise exception_to_click_error(scim_exc) from scim_exc
22
33
 
23
- payload = formatted_payload(response.model_dump(), indent)
24
- click.echo(payload)
34
+ click.echo(formatted_payload(response.model_dump(), indent))
25
35
 
26
36
 
27
- def create_factory(model):
37
+ def create_factory(model: type[Resource[Any]] | None) -> click.Command:
28
38
  if not model:
29
39
  raise ClickException("Invalid model")
30
40
 
@@ -42,7 +52,13 @@ def create_factory(model):
42
52
  )
43
53
  @from_pydantic("obj", model, exclude=exclude)
44
54
  @click.pass_context
45
- def create_command(ctx, indent, obj: model, *args, **kwargs):
55
+ def create_command(
56
+ ctx: click.Context,
57
+ indent: bool,
58
+ obj: Resource[Any] | None,
59
+ *args: Any,
60
+ **kwargs: Any,
61
+ ) -> None:
46
62
  r"""Perform a `SCIM POST <https://www.rfc-editor.org/rfc/rfc7644#section-3.3>`_ request on resources endpoint.
47
63
 
48
64
  Input data can be passed through parameters like :code:`--external-id`.
@@ -92,7 +108,7 @@ def create_factory(model):
92
108
  default=True,
93
109
  help="Indent JSON response payloads.",
94
110
  )
95
- def create_cli(ctx, indent):
111
+ def create_cli(ctx: click.Context, indent: bool) -> None:
96
112
  """Perform a `SCIM POST <https://www.rfc-editor.org/rfc/rfc7644#section-3.3>`_ request on resources endpoint.
97
113
 
98
114
  There are subcommands for all the available models, with dynamic attributes.
@@ -21,7 +21,7 @@ from .utils import formatted_payload
21
21
  help="Indent JSON response payloads.",
22
22
  )
23
23
  @click.pass_context
24
- def delete_cli(ctx, resource_type, id, indent):
24
+ def delete_cli(ctx: click.Context, resource_type: str, id: str, indent: bool) -> None:
25
25
  """Perform a `SCIM DELETE query <https://www.rfc-editor.org/rfc/rfc7644#section-3.6>`_ request.
26
26
 
27
27
  .. code-block:: bash
@@ -56,18 +56,18 @@ from .utils import formatted_payload
56
56
  help="Indent JSON response payloads.",
57
57
  )
58
58
  def query_cli(
59
- ctx,
59
+ ctx: click.Context,
60
60
  resource_type: str | None,
61
61
  id: str | None,
62
62
  attribute: list[str],
63
63
  excluded_attribute: list[str],
64
- start_index: int,
65
- count: int,
66
- filter: str,
67
- sort_by: str,
68
- sort_order: str,
64
+ start_index: int | None,
65
+ count: int | None,
66
+ filter: str | None,
67
+ sort_by: str | None,
68
+ sort_order: str | None,
69
69
  indent: bool,
70
- ):
70
+ ) -> None:
71
71
  """Perform a `SCIM GET <https://www.rfc-editor.org/rfc/rfc7644#section-3.4.1>`_ request on the :code:`RESOURCE_TYPE` endpoint.
72
72
 
73
73
  - If :code:`RESOURCE_TYPE` is :code:`user` and :code:`id` is `1234`, then the request will made on the :code:`/Users/1234` endpoint.
@@ -84,19 +84,20 @@ def query_cli(
84
84
  echo '{"startIndex": 50, "count": 10}' | query user
85
85
 
86
86
  """
87
- for model in (Schema, ResourceType, ServiceProviderConfig):
88
- ctx.obj["resource_models"][model.__name__.lower()] = model
87
+ for discovery_model in (Schema, ResourceType, ServiceProviderConfig):
88
+ ctx.obj["resource_models"][discovery_model.__name__.lower()] = discovery_model
89
89
 
90
+ model = None
90
91
  if resource_type:
91
92
  try:
92
- resource_type = ctx.obj["resource_models"][resource_type]
93
+ model = ctx.obj["resource_models"][resource_type]
93
94
  except KeyError as exc:
94
95
  ok_values = ", ".join(ctx.obj["resource_models"])
95
96
  raise ClickException(
96
97
  f"Unknown resource type '{resource_type}. Available values are: {ok_values}'"
97
98
  ) from exc
98
99
 
99
- single_resource = bool(id) or resource_type is ServiceProviderConfig
100
+ single_resource = bool(id) or model is ServiceProviderConfig
100
101
  listing_options = [
101
102
  name
102
103
  for name, value in (
@@ -119,26 +120,30 @@ def query_cli(
119
120
 
120
121
  elif single_resource:
121
122
  check_request_payload = True
122
- payload = ResponseParameters(
123
- attributes=attribute,
124
- excluded_attributes=excluded_attribute,
123
+ payload = ResponseParameters.model_validate(
124
+ {
125
+ "attributes": attribute,
126
+ "excluded_attributes": excluded_attribute,
127
+ }
125
128
  )
126
129
 
127
130
  else:
128
131
  check_request_payload = True
129
- payload = SearchRequest(
130
- attributes=attribute,
131
- excluded_attributes=excluded_attribute,
132
- start_index=start_index,
133
- count=count,
134
- filter=filter,
135
- sort_by=sort_by,
136
- sort_order=sort_order,
132
+ payload = SearchRequest.model_validate(
133
+ {
134
+ "attributes": attribute,
135
+ "excluded_attributes": excluded_attribute,
136
+ "start_index": start_index,
137
+ "count": count,
138
+ "filter": filter,
139
+ "sort_by": sort_by,
140
+ "sort_order": sort_order,
141
+ }
137
142
  )
138
143
 
139
144
  try:
140
145
  response = ctx.obj["client"].query(
141
- resource_type,
146
+ model,
142
147
  id,
143
148
  query_parameters=payload,
144
149
  check_request_payload=check_request_payload,
@@ -1,7 +1,13 @@
1
+ from typing import Any
2
+ from typing import cast
3
+
1
4
  import click
2
5
  from click import ClickException
3
6
  from pydanclick import from_pydantic
7
+ from scim2_client.engines.httpx2 import SyncSCIMClient
4
8
  from scim2_models import Context
9
+ from scim2_models import Error
10
+ from scim2_models import Resource
5
11
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
6
12
 
7
13
  from scim2_cli.utils import escape_options_help
@@ -14,18 +20,22 @@ from .utils import formatted_payload
14
20
  from .utils import unacceptable_fields
15
21
 
16
22
 
17
- def replace_payload(client, payload, indent):
23
+ def replace_payload(
24
+ client: SyncSCIMClient, payload: Resource[Any] | dict[str, Any], indent: bool
25
+ ) -> None:
18
26
  try:
19
- response = client.replace(payload, raise_scim_errors=False)
27
+ # Response payloads are always checked, so the client never returns a dict.
28
+ response = cast(
29
+ "Resource[Any] | Error", client.replace(payload, raise_scim_errors=False)
30
+ )
20
31
 
21
32
  except SCIM_EXCEPTIONS as scim_exc:
22
33
  raise exception_to_click_error(scim_exc) from scim_exc
23
34
 
24
- payload = formatted_payload(response.model_dump(), indent)
25
- click.echo(payload)
35
+ click.echo(formatted_payload(response.model_dump(), indent))
26
36
 
27
37
 
28
- def replace_factory(model):
38
+ def replace_factory(model: type[Resource[Any]] | None) -> click.Command:
29
39
  if not model:
30
40
  raise ClickException("Invalid model")
31
41
 
@@ -44,7 +54,13 @@ def replace_factory(model):
44
54
  )
45
55
  @from_pydantic("obj", model, exclude=exclude)
46
56
  @click.pass_context
47
- def replace_command(ctx, indent, obj: model, *args, **kwargs):
57
+ def replace_command(
58
+ ctx: click.Context,
59
+ indent: bool,
60
+ obj: Resource[Any] | None,
61
+ *args: Any,
62
+ **kwargs: Any,
63
+ ) -> None:
48
64
  r"""Perform a `SCIM PUT <https://www.rfc-editor.org/rfc/rfc7644#section-3.3>`_ request on resources endpoint.
49
65
 
50
66
  Input data can be passed through parameters like :code:`--external-id`.
@@ -99,7 +115,7 @@ def replace_factory(model):
99
115
  default=True,
100
116
  help="Indent JSON response payloads.",
101
117
  )
102
- def replace_cli(ctx, indent):
118
+ def replace_cli(ctx: click.Context, indent: bool) -> None:
103
119
  """Perform a `SCIM PUT <https://www.rfc-editor.org/rfc/rfc7644#section-3.5.1>`_ request on the resources endpoint.
104
120
 
105
121
  There are subcommands for all the available models, with dynamic attributes.
@@ -49,16 +49,16 @@ from .utils import formatted_payload
49
49
  help="Indent JSON response payloads.",
50
50
  )
51
51
  def search_cli(
52
- ctx,
52
+ ctx: click.Context,
53
53
  attribute: list[str],
54
54
  excluded_attribute: list[str],
55
- start_index: int,
56
- count: int,
57
- filter: str,
58
- sort_by: str,
59
- sort_order: str,
55
+ start_index: int | None,
56
+ count: int | None,
57
+ filter: str | None,
58
+ sort_by: str | None,
59
+ sort_order: str | None,
60
60
  indent: bool,
61
- ):
61
+ ) -> None:
62
62
  """Perform a `SCIM GET <https://www.rfc-editor.org/rfc/rfc7644#section-3.4.1>`_ request on the :code:`/.search` endpoint.
63
63
 
64
64
  Data passed in JSON format to stdin is sent as request arguments and all the other query arguments are ignored:
@@ -74,14 +74,16 @@ def search_cli(
74
74
 
75
75
  else:
76
76
  check_request_payload = True
77
- payload = SearchRequest(
78
- attributes=attribute,
79
- excluded_attributes=excluded_attribute,
80
- start_index=start_index,
81
- count=count,
82
- filter=filter,
83
- sort_by=sort_by,
84
- sort_order=sort_order,
77
+ payload = SearchRequest.model_validate(
78
+ {
79
+ "attributes": attribute,
80
+ "excluded_attributes": excluded_attribute,
81
+ "start_index": start_index,
82
+ "count": count,
83
+ "filter": filter,
84
+ "sort_by": sort_by,
85
+ "sort_order": sort_order,
86
+ }
85
87
  )
86
88
 
87
89
  try:
@@ -25,7 +25,9 @@ from .utils import escape_control_characters
25
25
  default=True,
26
26
  help="Fail on unexpected content types.",
27
27
  )
28
- def test_cli(ctx, verbose, check_status_code, check_content_type):
28
+ def test_cli(
29
+ ctx: click.Context, verbose: bool, check_status_code: bool, check_content_type: bool
30
+ ) -> None:
29
31
  """Perform a server SCIM compliance check using :doc:`scim2-tester <scim2_tester:index>`.
30
32
 
31
33
  .. code-block:: bash
@@ -1,11 +1,17 @@
1
1
  import json
2
2
  import re
3
+ from collections.abc import Callable
3
4
  from enum import StrEnum
5
+ from typing import Any
4
6
 
5
7
  import click
6
8
  from scim2_client import SCIMClientException
9
+ from scim2_models import BaseModel
10
+ from scim2_models import Context
11
+ from scim2_models import Mutability
12
+ from scim2_models import Resource
7
13
  from scim2_models import SCIMException
8
- from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
14
+ from sphinx_click.rst_to_ansi_formatter import RstToAnsiGroup
9
15
 
10
16
  DOC_URL = "https://scim2-cli.readthedocs.io/"
11
17
  INDENTATION_SIZE = 4
@@ -17,7 +23,7 @@ CONTROL_CHARACTERS = re.compile(
17
23
  )
18
24
 
19
25
 
20
- class HeaderType(click.ParamType):
26
+ class HeaderType(click.types.StringParamType):
21
27
  envvar_list_splitter = ";"
22
28
  name = "HEADER"
23
29
 
@@ -41,7 +47,7 @@ class Color(StrEnum):
41
47
  bright_white = "bright_white"
42
48
 
43
49
 
44
- def escape_control_characters(text) -> str:
50
+ def escape_control_characters(text: object) -> str:
45
51
  """Replace the control characters of a text by their escaped form.
46
52
 
47
53
  The texts coming from the server would otherwise be interpreted by the
@@ -52,17 +58,16 @@ def escape_control_characters(text) -> str:
52
58
  )
53
59
 
54
60
 
55
- def escape_options_help(command):
61
+ def escape_options_help(command: click.Command) -> click.Command:
56
62
  """Escape the option help texts pydanclick takes from the schema descriptions."""
57
63
  for param in command.params:
58
- if param.help:
64
+ if isinstance(param, click.Option) and param.help:
59
65
  param.help = escape_control_characters(param.help)
60
66
  return command
61
67
 
62
68
 
63
- def formatted_payload(obj, indent):
64
- indent = INDENTATION_SIZE if indent else False
65
- return json.dumps(obj, indent=indent)
69
+ def formatted_payload(obj: Any, indent: bool) -> str:
70
+ return json.dumps(obj, indent=INDENTATION_SIZE if indent else None)
66
71
 
67
72
 
68
73
  def split_headers(headers: list[str]) -> dict[str, str]:
@@ -76,32 +81,35 @@ def split_headers(headers: list[str]) -> dict[str, str]:
76
81
  }
77
82
 
78
83
 
79
- RSTCommand: click.Group = make_rst_to_ansi_formatter(DOC_URL, group=True)
80
-
81
-
82
- class ModelCommand(RSTCommand):
84
+ class ModelCommand(RstToAnsiGroup):
83
85
  """CLI commands that takes a model subcommand."""
84
86
 
85
- def __init__(self, *args, factory, **kwargs):
87
+ base_url = DOC_URL
88
+
89
+ def __init__(
90
+ self,
91
+ *args: Any,
92
+ factory: Callable[[type[Resource[Any]] | None], click.Command],
93
+ **kwargs: Any,
94
+ ) -> None:
86
95
  super().__init__(*args, **kwargs)
87
96
  self.factory = factory
88
97
 
89
- def list_commands(self, ctx):
98
+ def list_commands(self, ctx: click.Context) -> list[str]:
90
99
  ctx.ensure_object(dict)
91
100
  base = super().list_commands(ctx)
92
101
  lazy = sorted(ctx.obj.get("resource_models", {}).keys())
93
102
  return base + lazy
94
103
 
95
- def get_command(self, ctx, cmd_name):
104
+ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command:
96
105
  model = ctx.obj["resource_models"].get(cmd_name)
97
106
  return self.factory(model)
98
107
 
99
108
 
100
- def is_field_acceptable(context, model, field_name) -> bool:
109
+ def is_field_acceptable(
110
+ context: Context, model: type[BaseModel], field_name: str
111
+ ) -> bool:
101
112
  """Indicate whether a field is acceptable as part of a SCIM payload for a given context."""
102
- from scim2_models import Context
103
- from scim2_models import Mutability
104
-
105
113
  mutability = model.get_field_annotation(field_name, Mutability)
106
114
 
107
115
  if (
@@ -126,7 +134,7 @@ def is_field_acceptable(context, model, field_name) -> bool:
126
134
  return True
127
135
 
128
136
 
129
- def unacceptable_fields(context, model):
137
+ def unacceptable_fields(context: Context, model: type[BaseModel]) -> list[str]:
130
138
  excluded = [
131
139
  field_name
132
140
  for field_name in model.model_fields
@@ -136,7 +144,7 @@ def unacceptable_fields(context, model):
136
144
  return excluded
137
145
 
138
146
 
139
- def exception_to_click_error(exception):
147
+ def exception_to_click_error(exception: Exception) -> click.ClickException:
140
148
  message = str(exception)
141
149
  if hasattr(exception, "__notes__"):
142
150
  message += "\n" + "\n".join(exception.__notes__)
File without changes
File without changes
File without changes